diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b0f3cbe2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.DS_Store +/.idea/ +/config.yml diff --git a/common/enum/api_manager.go b/common/enum/api_manager.go new file mode 100644 index 00000000..08b47d4a --- /dev/null +++ b/common/enum/api_manager.go @@ -0,0 +1,37 @@ +package enum + +const ( + HeaderOptTypeAdd = "ADD" //新增或修改 + HeaderOptTypeDelete = "DELETE" //删除 + + MatchPositionHeader = "header" + MatchPositionQuery = "query" + MatchPositionCookie = "cookie" + + MatchTypeEqual = "EQUAL" //全等匹配 + MatchTypePrefix = "PREFIX" //前缀匹配 + MatchTypeSuffix = "SUFFIX" //后缀匹配 + MatchTypeSubstr = "SUBSTR" //子串匹配 + MatchTypeUnEqual = "UNEQUAL" //非等匹配 + MatchTypeNull = "NULL" //空值匹配 + MatchTypeExist = "EXIST" //存在匹配 + MatchTypeUnExist = "UNEXIST" //不存在匹配 + MatchTypeRegexp = "REGEXP" //区分大小写的正则匹配 + MatchTypeRegexpG = "REGEXPG" //不区分大小写的匹配 + MatchTypeAny = "ANY" //任意匹配 + + MethodGET = "GET" + MethodPOST = "POST" + MethodPUT = "PUT" + MethodDELETE = "DELETE" + MethodPATCH = "PATCH" + MethodHEAD = "HEAD" + MethodOPTIONS = "OPTIONS" + + RestfulLabel = "{rest}" + + //来源类型 + SourceSelfBuild = "self-build" //自建 + SourceImport = "import" //导入 + SourceSync = "sync" //同步 +) diff --git a/common/interface_to_all.go b/common/interface_to_all.go new file mode 100644 index 00000000..cda77839 --- /dev/null +++ b/common/interface_to_all.go @@ -0,0 +1,68 @@ +package common + +import ( + "fmt" + "strconv" +) + +func FmtIntFromInterface(val interface{}) int64 { + if val == nil { + return 0 + } + + switch ret := val.(type) { + case int8: + return int64(ret) + case int16: + return int64(ret) + case int32: + return int64(ret) + case int64: + return ret + case uint8: + return int64(ret) + case uint16: + return int64(ret) + case uint32: + return int64(ret) + case uint64: + return int64(ret) + case int: + return int64(ret) + default: + return 0 + } +} + +func FmtStringFromInterface(val interface{}) string { + if val == nil { + return "" + } + switch ret := val.(type) { + case string: + return ret + case int8, uint8, int16, uint16, int, uint, int64, uint64, float32, float64: + return fmt.Sprintf("%v", ret) + } + return "" +} + +func FmtFloatFromInterface(val interface{}) float64 { + if val == nil { + return 0 + } + + switch ret := val.(type) { + case float64: + return ret + case float32: + return float64(ret) + default: + return 0 + } +} + +func FloatToString(val float64) string { + float, _ := strconv.ParseFloat(fmt.Sprintf("%.2f", val), 64) + return strconv.FormatFloat(float, 'g', -1, 64) +} diff --git a/common/regexp.go b/common/regexp.go new file mode 100644 index 00000000..3e78ec44 --- /dev/null +++ b/common/regexp.go @@ -0,0 +1,131 @@ +package common + +import ( + "errors" + "fmt" + "regexp" +) + +type RegexpPattern string + +const ( + // EnglishOrNumber_ 英文开头,数字字母下划线组合 + EnglishOrNumber_ RegexpPattern = `^[a-zA-Z][a-zA-Z0-9_]*$` + // AnyEnglishOrNumber_ 数字字母下划线任意组合 + AnyEnglishOrNumber_ = `^[a-zA-Z0-9_]+$` + // UUIDExp UUID正则 数字字母横杠下划线任意组合 + UUIDExp = `^[a-zA-Z0-9-_]+$` + // DomainPortExp 域名或者域名:端口 + DomainPortExp = `^[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.?[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?(:[0-9]+)?$` + // IPPortExp IP:PORT + IPPortExp = `^((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}:[0-9]+$` + // SchemeIPPortExp scheme://IP:PORT + SchemeIPPortExp = `^[a-zA-z]+://((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}:[0-9]+$` + // CIDRIpv4Exp IPV4或者IPV4的CIDR + CIDRIpv4Exp = `^(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([1-9]|[1-2]\d|3[0-2]))?$` + // CheckPathIPPortExp (scheme://)?ip:port + CheckPathIPPortExp = `([a-zA-z]+://)?((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{1,2})){3}:[0-9]+` +) + +var ( + //环境变量专用 匹配字母开头,有字母数字下划线组合而成的字符串 环境变量专用 + variableRegexp = regexp.MustCompile(`^\${([a-zA-Z][a-zA-Z0-9_]*)}$`) + //筛选条件 APPKEY专用匹配字母开头,有字母数字下划线组合而成的字符串 + filterAppKeyRegexp = regexp.MustCompile(`^appkey{([a-zA-Z][a-zA-Z0-9_]*)}$`) + // 域名或者域名:PORT正则 + domainPortRegexp = regexp.MustCompile(DomainPortExp) + //IP:PORT 正则 + ipPortRegexp = regexp.MustCompile(IPPortExp) + //scheme://IP:PORT 正则 + schemeIpPortRegexp = regexp.MustCompile(SchemeIPPortExp) + //IPv4或者IPv4CIDR 正则 + cidrIpv4Regexp = regexp.MustCompile(CIDRIpv4Exp) + //checkIPPortRegexp 检查路径上是否包含xxx://ip:port的字符串 + checkIPPortRegexp = regexp.MustCompile(CheckPathIPPortExp) + + //restfulPathMatchRegexp 匹配包含restful参数的路径 + restfulPathMatchRegexp = regexp.MustCompile(`({[0-9a-zA-Z-_]+})+`) + //restfulParamMatchRegexp 匹配restful参数 {xxx} + restfulParamMatchRegexp = regexp.MustCompile(`^{[0-9a-zA-Z-_]+}$`) +) + +func IsMatchString(regexpPattern RegexpPattern, s string) error { + b, _ := regexp.MatchString(string(regexpPattern), s) + if b { + return nil + } + switch regexpPattern { + case EnglishOrNumber_: + return errors.New("只能使用英文字母、数字、下划线,英文字母开头") + case AnyEnglishOrNumber_: + return errors.New("只能使用英文字母、数字、下划线") + case UUIDExp: + return errors.New("只能使用英文字母、数字、横杠、下划线") + default: + return errors.New("非法字符串") + } +} + +// IsMatchVariable 判断字符串是否匹配环境变量标准格式${abc} +func IsMatchVariable(s string) bool { + return variableRegexp.MatchString(s) +} + +// IsMatchFilterAppKey 判断字符串是否匹配策略筛选条件key(应用)标准格式appkey{abc} +func IsMatchFilterAppKey(s string) bool { + return filterAppKeyRegexp.MatchString(s) +} + +// IsMatchDomainPort 判断字符串是否符合域名或者域名:port +func IsMatchDomainPort(s string) bool { + return domainPortRegexp.MatchString(s) +} + +// IsMatchIpPort 判断字符串是否符合ip:port +func IsMatchIpPort(s string) bool { + return ipPortRegexp.MatchString(s) +} + +// IsMatchSchemeIpPort 判断字符串是否符合scheme://ip:port +func IsMatchSchemeIpPort(s string) bool { + return schemeIpPortRegexp.MatchString(s) +} + +// IsMatchCIDRIpv4 判断字符串是否符合ipv4或者ipv4的cidr +func IsMatchCIDRIpv4(s string) bool { + return cidrIpv4Regexp.MatchString(s) +} + +// GetVariableKey 从环境变量标准格式${abc}中取得key abc +func GetVariableKey(s string) string { + return variableRegexp.ReplaceAllString(s, "$1") +} + +// GetFilterAppKey 从标准格式appkey{abc}中取得key abc +func GetFilterAppKey(s string) string { + return filterAppKeyRegexp.ReplaceAllString(s, "$1") +} + +func SetFilterAppKey(key string) string { + return fmt.Sprintf("appkey{%s}", key) +} + +// IsRestfulPath 检查路径是否有restful参数 +func IsRestfulPath(path string) bool { + return restfulPathMatchRegexp.MatchString(path) +} + +// IsRestfulParam 检查是否为restful参数 +func IsRestfulParam(param string) bool { + return restfulParamMatchRegexp.MatchString(param) +} + +// ReplaceRestfulPath 将restful路径转换成apinto的正则匹配路径 +func ReplaceRestfulPath(path, replaceStr string) string { + return restfulPathMatchRegexp.ReplaceAllString(path, replaceStr) +} + +// CheckPathContainsIPPort 检查路径中是否包含xxx://ip:port +func CheckPathContainsIPPort(path string) bool { + return checkIPPortRegexp.MatchString(path) +} diff --git a/common/version/version.go b/common/version/version.go new file mode 100644 index 00000000..bcacbb3c --- /dev/null +++ b/common/version/version.go @@ -0,0 +1,40 @@ +package version + +import ( + "bytes" + "fmt" + + "github.com/urfave/cli/v2" +) + +// These should be set via go build -ldflags -X 'xxxx'. +var Version = "unknown" +var goVersion = "unknown" +var gitCommit = "unknown" +var BuildTime = "unknown" +var buildUser = "unknown" + +//var eoscVersion = "unknown" + +var profileInfo []byte + +func init() { + buffer := &bytes.Buffer{} + fmt.Fprintf(buffer, "Apinto version: %s\n", Version) + fmt.Fprintf(buffer, "Golang version: %s\n", goVersion) + fmt.Fprintf(buffer, "Git commit hash: %s\n", gitCommit) + fmt.Fprintf(buffer, "Built on: %s\n", BuildTime) + fmt.Fprintf(buffer, "Built by: %s\n", buildUser) + //fmt.Fprintf(buffer, "Built by eosc version: %s\n", eoscVersion) + profileInfo = buffer.Bytes() +} + +func Build() *cli.Command { + return &cli.Command{ + Name: "version", + Action: func(context *cli.Context) error { + fmt.Print(string(profileInfo)) + return nil + }, + } +} diff --git a/controller/api/api.go b/controller/api/api.go new file mode 100644 index 00000000..c1c2d0fd --- /dev/null +++ b/controller/api/api.go @@ -0,0 +1,43 @@ +package api + +import ( + "reflect" + + "github.com/gin-gonic/gin" + + "github.com/eolinker/go-common/autowire" + + api_dto "github.com/APIParkLab/APIPark/module/api/dto" +) + +type IAPIController interface { + // Detail 获取API详情 + Detail(ctx *gin.Context, serviceId string, apiId string) (*api_dto.ApiDetail, error) + // SimpleDetail 获取API简要详情 + SimpleDetail(ctx *gin.Context, serviceId string, apiId string) (*api_dto.ApiSimpleDetail, error) + // Search 获取API列表 + Search(ctx *gin.Context, keyword string, serviceId string) ([]*api_dto.ApiItem, error) + // SimpleSearch 获取API简要列表 + SimpleSearch(ctx *gin.Context, keyword string, serviceId string) ([]*api_dto.ApiSimpleItem, error) + //SimpleList(ctx *gin.Context, serviceId string) ([]*api_dto.ApiSimpleItem, error) + // Create 创建API + Create(ctx *gin.Context, serviceId string, dto *api_dto.CreateApi) (*api_dto.ApiSimpleDetail, error) + // Edit 编辑API + Edit(ctx *gin.Context, serviceId string, apiId string, dto *api_dto.EditApi) (*api_dto.ApiSimpleDetail, error) + // Delete 删除API + Delete(ctx *gin.Context, serviceId string, apiId string) error + // Copy 复制API + Copy(ctx *gin.Context, serviceId string, apiId string, dto *api_dto.CreateApi) (*api_dto.ApiSimpleDetail, error) + // ApiDocDetail 获取API文档详情 + ApiDocDetail(ctx *gin.Context, serviceId string, apiId string) (*api_dto.ApiDocDetail, error) + // ApiProxyDetail 获取API代理详情 + ApiProxyDetail(ctx *gin.Context, serviceId string, apiId string) (*api_dto.ApiProxyDetail, error) + // Prefix 获取API前缀 + Prefix(ctx *gin.Context, serviceId string) (string, bool, error) +} + +func init() { + autowire.Auto[IAPIController](func() reflect.Value { + return reflect.ValueOf(new(imlAPIController)) + }) +} diff --git a/controller/api/iml.go b/controller/api/iml.go new file mode 100644 index 00000000..78a021f0 --- /dev/null +++ b/controller/api/iml.go @@ -0,0 +1,65 @@ +package api + +import ( + "github.com/APIParkLab/APIPark/module/api" + api_dto "github.com/APIParkLab/APIPark/module/api/dto" + "github.com/gin-gonic/gin" +) + +var _ IAPIController = (*imlAPIController)(nil) + +type imlAPIController struct { + module api.IApiModule `autowired:""` +} + +//func (i *imlAPIController) SimpleList(ctx *gin.Context, serviceId string) ([]*api_dto.ApiSimpleItem, error) { +// return i.module.SimpleList(ctx, serviceId) +//} + +func (i *imlAPIController) Detail(ctx *gin.Context, serviceId string, apiId string) (*api_dto.ApiDetail, error) { + return i.module.Detail(ctx, serviceId, apiId) +} + +func (i *imlAPIController) SimpleDetail(ctx *gin.Context, serviceId string, apiId string) (*api_dto.ApiSimpleDetail, error) { + return i.module.SimpleDetail(ctx, serviceId, apiId) +} + +func (i *imlAPIController) Search(ctx *gin.Context, keyword string, serviceId string) ([]*api_dto.ApiItem, error) { + return i.module.Search(ctx, keyword, serviceId) +} + +func (i *imlAPIController) SimpleSearch(ctx *gin.Context, keyword string, serviceId string) ([]*api_dto.ApiSimpleItem, error) { + return i.module.SimpleSearch(ctx, keyword, serviceId) +} + +func (i *imlAPIController) Create(ctx *gin.Context, serviceId string, dto *api_dto.CreateApi) (*api_dto.ApiSimpleDetail, error) { + return i.module.Create(ctx, serviceId, dto) +} + +func (i *imlAPIController) Edit(ctx *gin.Context, serviceId string, apiId string, dto *api_dto.EditApi) (*api_dto.ApiSimpleDetail, error) { + return i.module.Edit(ctx, serviceId, apiId, dto) +} + +func (i *imlAPIController) Delete(ctx *gin.Context, serviceId string, apiId string) error { + return i.module.Delete(ctx, serviceId, apiId) +} + +func (i *imlAPIController) Copy(ctx *gin.Context, serviceId string, apiId string, dto *api_dto.CreateApi) (*api_dto.ApiSimpleDetail, error) { + return i.module.Copy(ctx, serviceId, apiId, dto) +} + +func (i *imlAPIController) ApiDocDetail(ctx *gin.Context, serviceId string, apiId string) (*api_dto.ApiDocDetail, error) { + return i.module.ApiDocDetail(ctx, serviceId, apiId) +} + +func (i *imlAPIController) ApiProxyDetail(ctx *gin.Context, serviceId string, apiId string) (*api_dto.ApiProxyDetail, error) { + return i.module.ApiProxyDetail(ctx, serviceId, apiId) +} + +func (i *imlAPIController) Prefix(ctx *gin.Context, serviceId string) (string, bool, error) { + prefix, err := i.module.Prefix(ctx, serviceId) + if err != nil { + return "", false, err + } + return prefix, true, nil +} diff --git a/controller/application-authorization/authorization.go b/controller/application-authorization/authorization.go new file mode 100644 index 00000000..0ae70261 --- /dev/null +++ b/controller/application-authorization/authorization.go @@ -0,0 +1,32 @@ +package application_authorization + +import ( + "reflect" + + "github.com/gin-gonic/gin" + + "github.com/eolinker/go-common/autowire" + + application_authorization_dto "github.com/APIParkLab/APIPark/module/application-authorization/dto" +) + +type IAuthorizationController interface { + // AddAuthorization 添加项目鉴权信息 + AddAuthorization(ctx *gin.Context, pid string, info *application_authorization_dto.CreateAuthorization) (*application_authorization_dto.Authorization, error) + // EditAuthorization 修改项目鉴权信息 + EditAuthorization(ctx *gin.Context, pid string, aid string, info *application_authorization_dto.EditAuthorization) (*application_authorization_dto.Authorization, error) + // DeleteAuthorization 删除项目鉴权 + DeleteAuthorization(ctx *gin.Context, pid string, aid string) error + // Authorizations 获取项目鉴权列表 + Authorizations(ctx *gin.Context, pid string) ([]*application_authorization_dto.AuthorizationItem, error) + // Detail 获取项目鉴权详情(弹窗用) + Detail(ctx *gin.Context, pid string, aid string) ([]application_authorization_dto.DetailItem, error) + // Info 获取项目鉴权详情 + Info(ctx *gin.Context, pid string, aid string) (*application_authorization_dto.Authorization, error) +} + +func init() { + autowire.Auto[IAuthorizationController](func() reflect.Value { + return reflect.ValueOf(new(imlAuthorizationController)) + }) +} diff --git a/controller/application-authorization/iml.go b/controller/application-authorization/iml.go new file mode 100644 index 00000000..46976596 --- /dev/null +++ b/controller/application-authorization/iml.go @@ -0,0 +1,37 @@ +package application_authorization + +import ( + application_authorization "github.com/APIParkLab/APIPark/module/application-authorization" + application_authorization_dto "github.com/APIParkLab/APIPark/module/application-authorization/dto" + "github.com/gin-gonic/gin" +) + +var _ IAuthorizationController = (*imlAuthorizationController)(nil) + +type imlAuthorizationController struct { + module application_authorization.IAuthorizationModule `autowired:""` +} + +func (i *imlAuthorizationController) AddAuthorization(ctx *gin.Context, pid string, info *application_authorization_dto.CreateAuthorization) (*application_authorization_dto.Authorization, error) { + return i.module.AddAuthorization(ctx, pid, info) +} + +func (i *imlAuthorizationController) EditAuthorization(ctx *gin.Context, pid string, aid string, info *application_authorization_dto.EditAuthorization) (*application_authorization_dto.Authorization, error) { + return i.module.EditAuthorization(ctx, pid, aid, info) +} + +func (i *imlAuthorizationController) DeleteAuthorization(ctx *gin.Context, pid string, aid string) error { + return i.module.DeleteAuthorization(ctx, pid, aid) +} + +func (i *imlAuthorizationController) Authorizations(ctx *gin.Context, pid string) ([]*application_authorization_dto.AuthorizationItem, error) { + return i.module.Authorizations(ctx, pid) +} + +func (i *imlAuthorizationController) Detail(ctx *gin.Context, pid string, aid string) ([]application_authorization_dto.DetailItem, error) { + return i.module.Detail(ctx, pid, aid) +} + +func (i *imlAuthorizationController) Info(ctx *gin.Context, pid string, aid string) (*application_authorization_dto.Authorization, error) { + return i.module.Info(ctx, pid, aid) +} diff --git a/controller/catalogue/catalogue.go b/controller/catalogue/catalogue.go new file mode 100644 index 00000000..a1feceb5 --- /dev/null +++ b/controller/catalogue/catalogue.go @@ -0,0 +1,36 @@ +package catalogue + +import ( + "github.com/gin-gonic/gin" + "reflect" + + tag_dto "github.com/APIParkLab/APIPark/module/tag/dto" + + catalogue_dto "github.com/APIParkLab/APIPark/module/catalogue/dto" + + "github.com/eolinker/go-common/autowire" +) + +type ICatalogueController interface { + // Search 搜索目录、标签列表 + Search(ctx *gin.Context, keyword string) ([]*catalogue_dto.Item, []*tag_dto.Item, error) + // Create 创建目录 + Create(ctx *gin.Context, input *catalogue_dto.CreateCatalogue) error + // Edit 修改目录 + Edit(ctx *gin.Context, id string, input *catalogue_dto.EditCatalogue) error + // Delete 删除目录 + Delete(ctx *gin.Context, id string) error + // Services 服务列表 + Services(ctx *gin.Context, keyword string) ([]*catalogue_dto.ServiceItem, error) + // ServiceDetail 服务详情 + ServiceDetail(ctx *gin.Context, sid string) (*catalogue_dto.ServiceDetail, error) + // Subscribe 订阅服务 + Subscribe(ctx *gin.Context, subscribeInfo *catalogue_dto.SubscribeService) error + Sort(ctx *gin.Context, sorts *[]*catalogue_dto.SortItem) error +} + +func init() { + autowire.Auto[ICatalogueController](func() reflect.Value { + return reflect.ValueOf(new(imlCatalogueController)) + }) +} diff --git a/controller/catalogue/iml.go b/controller/catalogue/iml.go new file mode 100644 index 00000000..56dfb0c1 --- /dev/null +++ b/controller/catalogue/iml.go @@ -0,0 +1,62 @@ +package catalogue + +import ( + "github.com/APIParkLab/APIPark/module/catalogue" + catalogue_dto "github.com/APIParkLab/APIPark/module/catalogue/dto" + "github.com/APIParkLab/APIPark/module/tag" + tag_dto "github.com/APIParkLab/APIPark/module/tag/dto" + "github.com/gin-gonic/gin" +) + +var ( + _ ICatalogueController = (*imlCatalogueController)(nil) +) + +type imlCatalogueController struct { + catalogueModule catalogue.ICatalogueModule `autowired:""` + tagModule tag.ITagModule `autowired:""` +} + +func (i *imlCatalogueController) Sort(ctx *gin.Context, sorts *[]*catalogue_dto.SortItem) error { + return i.catalogueModule.Sort(ctx, *sorts) +} + +func (i *imlCatalogueController) Subscribe(ctx *gin.Context, subscribeInfo *catalogue_dto.SubscribeService) error { + return i.catalogueModule.Subscribe(ctx, subscribeInfo) +} + +func (i *imlCatalogueController) ServiceDetail(ctx *gin.Context, sid string) (*catalogue_dto.ServiceDetail, error) { + return i.catalogueModule.ServiceDetail(ctx, sid) +} + +func (i *imlCatalogueController) Search(ctx *gin.Context, keyword string) ([]*catalogue_dto.Item, []*tag_dto.Item, error) { + catalogues, err := i.catalogueModule.Search(ctx, keyword) + if err != nil { + return nil, nil, err + } + tags, err := i.tagModule.Search(ctx, keyword) + if err != nil { + return nil, nil, err + } + return catalogues, tags, nil +} + +func (i *imlCatalogueController) Create(ctx *gin.Context, input *catalogue_dto.CreateCatalogue) error { + return i.catalogueModule.Create(ctx, input) +} + +func (i *imlCatalogueController) Edit(ctx *gin.Context, id string, input *catalogue_dto.EditCatalogue) error { + return i.catalogueModule.Edit(ctx, id, input) +} + +func (i *imlCatalogueController) Delete(ctx *gin.Context, id string) error { + return i.catalogueModule.Delete(ctx, id) +} + +func (i *imlCatalogueController) Services(ctx *gin.Context, keyword string) ([]*catalogue_dto.ServiceItem, error) { + items, err := i.catalogueModule.Services(ctx, keyword) + if err != nil { + return nil, err + } + return items, nil +} diff --git a/controller/certificate/certificate.go b/controller/certificate/certificate.go new file mode 100644 index 00000000..14b14210 --- /dev/null +++ b/controller/certificate/certificate.go @@ -0,0 +1,23 @@ +package certificate + +import ( + "reflect" + + certificate_dto "github.com/APIParkLab/APIPark/module/certificate/dto" + "github.com/eolinker/go-common/autowire" + "github.com/gin-gonic/gin" +) + +type ICertificateController interface { + Create(ctx *gin.Context, create *certificate_dto.FileInput) error + Update(ctx *gin.Context, id string, edit *certificate_dto.FileInput) error + ListForPartition(ctx *gin.Context) ([]*certificate_dto.Certificate, error) + Detail(ctx *gin.Context, id string) (*certificate_dto.Certificate, *certificate_dto.File, error) + Delete(ctx *gin.Context, id string) (string, error) +} + +func init() { + autowire.Auto[ICertificateController](func() reflect.Value { + return reflect.ValueOf(new(imlCertificate)) + }) +} diff --git a/controller/certificate/iml.go b/controller/certificate/iml.go new file mode 100644 index 00000000..60425e7c --- /dev/null +++ b/controller/certificate/iml.go @@ -0,0 +1,39 @@ +package certificate + +import ( + "github.com/APIParkLab/APIPark/module/certificate" + certificate_dto "github.com/APIParkLab/APIPark/module/certificate/dto" + "github.com/gin-gonic/gin" +) + +var ( + _ ICertificateController = (*imlCertificate)(nil) +) + +type imlCertificate struct { + module certificate.ICertificateModule `autowired:""` +} + +func (c *imlCertificate) Create(ctx *gin.Context, create *certificate_dto.FileInput) error { + return c.module.Create(ctx, create) +} + +func (c *imlCertificate) Update(ctx *gin.Context, id string, edit *certificate_dto.FileInput) error { + return c.module.Update(ctx, id, edit) +} + +func (c *imlCertificate) ListForPartition(ctx *gin.Context) ([]*certificate_dto.Certificate, error) { + return c.module.List(ctx) +} + +func (c *imlCertificate) Detail(ctx *gin.Context, id string) (*certificate_dto.Certificate, *certificate_dto.File, error) { + return c.module.Detail(ctx, id) +} + +func (c *imlCertificate) Delete(ctx *gin.Context, id string) (string, error) { + err := c.module.Delete(ctx, id) + if err != nil { + return "", err + } + return id, nil +} diff --git a/controller/cluster/cluster.go b/controller/cluster/cluster.go new file mode 100644 index 00000000..be590411 --- /dev/null +++ b/controller/cluster/cluster.go @@ -0,0 +1,22 @@ +package cluster + +import ( + "reflect" + + cluster_dto "github.com/APIParkLab/APIPark/module/cluster/dto" + + "github.com/eolinker/go-common/autowire" + "github.com/gin-gonic/gin" +) + +type IClusterController interface { + Nodes(ctx *gin.Context, clusterId string) ([]*cluster_dto.Node, error) + ResetCluster(ctx *gin.Context, clusterId string, input *cluster_dto.ResetCluster) ([]*cluster_dto.Node, error) + Check(ctx *gin.Context, input *cluster_dto.CheckCluster) ([]*cluster_dto.Node, error) +} + +func init() { + autowire.Auto[IClusterController](func() reflect.Value { + return reflect.ValueOf(new(imlCluster)) + }) +} diff --git a/controller/cluster/iml.go b/controller/cluster/iml.go new file mode 100644 index 00000000..d67de3d0 --- /dev/null +++ b/controller/cluster/iml.go @@ -0,0 +1,73 @@ +package cluster + +import ( + "github.com/APIParkLab/APIPark/module/cluster" + cluster_dto "github.com/APIParkLab/APIPark/module/cluster/dto" + "github.com/gin-gonic/gin" +) + +var ( + _ IClusterController = (*imlCluster)(nil) +) + +type imlCluster struct { + module cluster.IClusterModule `autowired:""` +} + +func (p *imlCluster) Nodes(ctx *gin.Context, clusterId string) ([]*cluster_dto.Node, error) { + if clusterId == "" { + clusterId = "default" + } + return p.module.ClusterNodes(ctx, clusterId) +} + +func (p *imlCluster) ResetCluster(ctx *gin.Context, clusterId string, input *cluster_dto.ResetCluster) ([]*cluster_dto.Node, error) { + if clusterId == "" { + clusterId = "default" + } + return p.module.ResetCluster(ctx, clusterId, input.ManagerAddress) +} + +func (p *imlCluster) Check(ctx *gin.Context, input *cluster_dto.CheckCluster) ([]*cluster_dto.Node, error) { + return p.module.CheckCluster(ctx, input.Address) +} + +// +//func (p *imlCluster) SimpleWithCluster(ctx *gin.Context) ([]*parition_dto.SimpleWithCluster, error) { +// return p.module.SimpleWithCluster(ctx) +//} +// +//func (p *imlCluster) Delete(ctx *gin.Context, id string) (string, error) { +// err := p.module.Delete(ctx, id) +// if err != nil { +// return "", err +// } +// return id, nil +//} +// +//func (p *imlCluster) Search(ctx *gin.Context, keyword string) ([]*parition_dto.Item, error) { +// return p.module.Search(ctx, keyword) +//} +// +//func (p *imlCluster) Simple(ctx *gin.Context) ([]*parition_dto.Simple, error) { +// return p.module.Simple(ctx) +//} +// +//func (p *imlCluster) Info(ctx *gin.Context, id string) (*parition_dto.Detail, error) { +// if id == "" { +// return nil, errors.New("id is empty") +// } +// return p.module.Get(ctx, id) +//} +// +//func (p *imlCluster) Update(ctx *gin.Context, id string, input *parition_dto.Edit) (*parition_dto.Detail, error) { +// return p.module.Update(ctx, id, input) +//} +// +//func (p *imlCluster) Create(ctx *gin.Context, input *parition_dto.Create) (*parition_dto.Detail, string, auto.TimeLabel, error) { +// detail, err := p.module.CreatePartition(ctx, input) +// if err != nil { +// return nil, "", auto.TimeLabel{}, err +// } +// return detail, detail.Id, detail.UpdateTime, nil +//} diff --git a/controller/common/common.go b/controller/common/common.go new file mode 100644 index 00000000..5f92607e --- /dev/null +++ b/controller/common/common.go @@ -0,0 +1,17 @@ +package common + +import ( + "github.com/eolinker/go-common/autowire" + "github.com/gin-gonic/gin" + "reflect" +) + +type ICommonController interface { + Version(ctx *gin.Context) (string, string, error) +} + +func init() { + autowire.Auto[ICommonController](func() reflect.Value { + return reflect.ValueOf(new(imlCommonController)) + }) +} diff --git a/controller/common/iml.go b/controller/common/iml.go new file mode 100644 index 00000000..6034cbae --- /dev/null +++ b/controller/common/iml.go @@ -0,0 +1,14 @@ +package common + +import ( + "github.com/APIParkLab/APIPark/common/version" + "github.com/gin-gonic/gin" +) + +var _ ICommonController = (*imlCommonController)(nil) + +type imlCommonController struct{} + +func (i imlCommonController) Version(ctx *gin.Context) (string, string, error) { + return version.Version, version.BuildTime, nil +} diff --git a/controller/controller.go b/controller/controller.go new file mode 100644 index 00000000..ce721571 --- /dev/null +++ b/controller/controller.go @@ -0,0 +1,5 @@ +// Description: This package is used to handle all the request from the client +// and return the response to the client. +// 只能使用 module 下面的封装好的接口,不能直接使用 service 下面的接口 + +package controller diff --git a/controller/dynamic-module/dynamic-module.go b/controller/dynamic-module/dynamic-module.go new file mode 100644 index 00000000..820216bd --- /dev/null +++ b/controller/dynamic-module/dynamic-module.go @@ -0,0 +1,29 @@ +package dynamic_module + +import ( + "reflect" + + dynamic_module_dto "github.com/APIParkLab/APIPark/module/dynamic-module/dto" + "github.com/eolinker/go-common/autowire" + "github.com/gin-gonic/gin" +) + +type IDynamicModuleController interface { + Create(ctx *gin.Context, module string, input *dynamic_module_dto.CreateDynamicModule) (*dynamic_module_dto.DynamicModule, error) + Edit(ctx *gin.Context, module string, id string, input *dynamic_module_dto.EditDynamicModule) (*dynamic_module_dto.DynamicModule, error) + Delete(ctx *gin.Context, module string, ids string) error + Get(ctx *gin.Context, module string, id string) (*dynamic_module_dto.DynamicModule, error) + List(ctx *gin.Context, module string, keyword string, cluster string, page string, pageSize string) ([]map[string]interface{}, *dynamic_module_dto.PluginInfo, int64, error) + Render(ctx *gin.Context, module string) (*dynamic_module_dto.PluginBasic, map[string]interface{}, error) + ModuleDrivers(ctx *gin.Context, group string) ([]*dynamic_module_dto.ModuleDriver, error) + Online(ctx *gin.Context, module string, id string, partitionInput *dynamic_module_dto.ClusterInput) error + Offline(ctx *gin.Context, module string, id string, partitionInput *dynamic_module_dto.ClusterInput) error + //PartitionStatuses(ctx *gin.Context, module string, keyword string, page string, pageSize string) (map[string]map[string]string, error) + //PartitionStatus(ctx *gin.Context, module string, id string) (*dynamic_module_dto.OnlineInfo, error) +} + +func init() { + autowire.Auto[IDynamicModuleController](func() reflect.Value { + return reflect.ValueOf(new(imlDynamicModuleController)) + }) +} diff --git a/controller/dynamic-module/iml.go b/controller/dynamic-module/iml.go new file mode 100644 index 00000000..3c51cd73 --- /dev/null +++ b/controller/dynamic-module/iml.go @@ -0,0 +1,109 @@ +package dynamic_module + +import ( + "encoding/json" + "strconv" + + dynamic_module "github.com/APIParkLab/APIPark/module/dynamic-module" + dynamic_module_dto "github.com/APIParkLab/APIPark/module/dynamic-module/dto" + "github.com/gin-gonic/gin" +) + +var _ IDynamicModuleController = (*imlDynamicModuleController)(nil) + +type imlDynamicModuleController struct { + module dynamic_module.IDynamicModuleModule `autowired:""` +} + +func (i *imlDynamicModuleController) Online(ctx *gin.Context, module string, id string, partitionInput *dynamic_module_dto.ClusterInput) error { + return i.module.Online(ctx, module, id, partitionInput) +} + +func (i *imlDynamicModuleController) Offline(ctx *gin.Context, module string, id string, partitionInput *dynamic_module_dto.ClusterInput) error { + return i.module.Offline(ctx, module, id, partitionInput) +} + +//func (i *imlDynamicModuleController) PartitionStatuses(ctx *gin.Context, module string, keyword string, page string, pageSize string) (map[string]map[string]string, error) { +// p, err := strconv.Atoi(page) +// if err != nil { +// p = 1 +// } +// ps, err := strconv.Atoi(pageSize) +// if err != nil { +// ps = 20 +// } +// return i.module.PartitionStatuses(ctx, module, keyword, p, ps) +//} +// +//func (i *imlDynamicModuleController) PartitionStatus(ctx *gin.Context, module string, id string) (*dynamic_module_dto.OnlineInfo, error) { +// return i.module.PartitionStatus(ctx, module, id) +//} + +func (i *imlDynamicModuleController) ModuleDrivers(ctx *gin.Context, group string) ([]*dynamic_module_dto.ModuleDriver, error) { + return i.module.ModuleDrivers(ctx, group) +} + +func (i *imlDynamicModuleController) Render(ctx *gin.Context, module string) (*dynamic_module_dto.PluginBasic, map[string]interface{}, error) { + render, err := i.module.Render(ctx, module) + if err != nil { + return nil, nil, err + } + pluginInfo, err := i.module.PluginInfo(ctx, module) + if err != nil { + return nil, nil, err + } + return pluginInfo.PluginBasic, render, nil +} + +func (i *imlDynamicModuleController) Create(ctx *gin.Context, module string, input *dynamic_module_dto.CreateDynamicModule) (*dynamic_module_dto.DynamicModule, error) { + return i.module.Create(ctx, module, input) +} + +func (i *imlDynamicModuleController) Edit(ctx *gin.Context, module string, id string, input *dynamic_module_dto.EditDynamicModule) (*dynamic_module_dto.DynamicModule, error) { + return i.module.Edit(ctx, module, id, input) +} + +func (i *imlDynamicModuleController) Delete(ctx *gin.Context, module string, idStr string) error { + ids := make([]string, 0) + err := json.Unmarshal([]byte(idStr), &ids) + if err != nil { + return err + } + if len(ids) == 0 { + return nil + } + return i.module.Delete(ctx, module, ids) +} + +func (i *imlDynamicModuleController) Get(ctx *gin.Context, module string, id string) (*dynamic_module_dto.DynamicModule, error) { + return i.module.Get(ctx, module, id) +} + +func (i *imlDynamicModuleController) List(ctx *gin.Context, module string, keyword string, clusterId string, page string, pageSize string) ([]map[string]interface{}, *dynamic_module_dto.PluginInfo, int64, error) { + p, err := strconv.Atoi(page) + if err != nil { + p = 1 + } + ps, err := strconv.Atoi(pageSize) + if err != nil { + ps = 20 + + } + list, total, err := i.module.List(ctx, module, keyword, p, ps) + if err != nil { + return nil, nil, 0, err + } + //if clusterId == "" { + // clusterId = "[]" + //} + //ids := make([]string, 0) + //err = json.Unmarshal([]byte(clusterId), &ids) + //if err != nil { + // return nil, nil, 0, err + //} + plugin, err := i.module.PluginInfo(ctx, module) + if err != nil { + return nil, nil, 0, err + } + return list, plugin, total, nil +} diff --git a/controller/my_team/iml.go b/controller/my_team/iml.go new file mode 100644 index 00000000..a5438e8f --- /dev/null +++ b/controller/my_team/iml.go @@ -0,0 +1,52 @@ +package my_team + +import ( + my_team "github.com/APIParkLab/APIPark/module/my-team" + team_dto "github.com/APIParkLab/APIPark/module/my-team/dto" + "github.com/gin-gonic/gin" +) + +var ( + _ ITeamController = (*imlTeamController)(nil) +) + +type imlTeamController struct { + module my_team.ITeamModule `autowired:""` +} + +func (c *imlTeamController) UpdateMemberRole(ctx *gin.Context, id string, input *team_dto.UpdateMemberRole) error { + return c.module.UpdateMemberRole(ctx, id, input) +} + +func (c *imlTeamController) GetTeam(ctx *gin.Context, id string) (*team_dto.Team, error) { + return c.module.GetTeam(ctx, id) +} + +func (c *imlTeamController) Search(ctx *gin.Context, keyword string) ([]*team_dto.Item, error) { + + return c.module.Search(ctx, keyword) +} + +func (c *imlTeamController) EditTeam(ctx *gin.Context, id string, team *team_dto.EditTeam) (*team_dto.Team, error) { + return c.module.Edit(ctx, id, team) +} + +func (c *imlTeamController) SimpleTeams(ctx *gin.Context, keyword string) ([]*team_dto.SimpleTeam, error) { + return c.module.SimpleTeams(ctx, keyword) +} + +func (c *imlTeamController) AddMember(ctx *gin.Context, id string, users *team_dto.UserIDs) error { + return c.module.AddMember(ctx, id, users.Users...) +} + +func (c *imlTeamController) RemoveMember(ctx *gin.Context, id string, uuid string) error { + return c.module.RemoveMember(ctx, id, uuid) +} + +func (c *imlTeamController) Members(ctx *gin.Context, id string, keyword string) ([]*team_dto.Member, error) { + return c.module.Members(ctx, id, keyword) +} + +func (c *imlTeamController) SimpleMembers(ctx *gin.Context, id string, keyword string) ([]*team_dto.SimpleMember, error) { + return c.module.SimpleMembers(ctx, id, keyword) +} diff --git a/controller/my_team/team.go b/controller/my_team/team.go new file mode 100644 index 00000000..a755f67c --- /dev/null +++ b/controller/my_team/team.go @@ -0,0 +1,28 @@ +package my_team + +import ( + "reflect" + + team_dto "github.com/APIParkLab/APIPark/module/my-team/dto" + "github.com/eolinker/go-common/autowire" + "github.com/gin-gonic/gin" +) + +type ITeamController interface { + // GetTeam 获取团队信息 + GetTeam(ctx *gin.Context, id string) (*team_dto.Team, error) + Search(ctx *gin.Context, keyword string) ([]*team_dto.Item, error) + EditTeam(ctx *gin.Context, id string, team *team_dto.EditTeam) (*team_dto.Team, error) + SimpleTeams(ctx *gin.Context, keyword string) ([]*team_dto.SimpleTeam, error) + AddMember(ctx *gin.Context, id string, users *team_dto.UserIDs) error + RemoveMember(ctx *gin.Context, id string, uuid string) error + Members(ctx *gin.Context, id string, keyword string) ([]*team_dto.Member, error) + SimpleMembers(ctx *gin.Context, id string, keyword string) ([]*team_dto.SimpleMember, error) + UpdateMemberRole(ctx *gin.Context, id string, input *team_dto.UpdateMemberRole) error +} + +func init() { + autowire.Auto[ITeamController](func() reflect.Value { + return reflect.ValueOf(new(imlTeamController)) + }) +} diff --git a/controller/permit_system/iml.go b/controller/permit_system/iml.go new file mode 100644 index 00000000..3cbb0753 --- /dev/null +++ b/controller/permit_system/iml.go @@ -0,0 +1,24 @@ +package permit_system + +import ( + "github.com/APIParkLab/APIPark/module/permit/system" + "github.com/eolinker/go-common/autowire" + "github.com/gin-gonic/gin" +) + +var ( + _ ISystemPermitController = (*imlSystemPermitController)(nil) + _ autowire.Complete = (*imlSystemPermitController)(nil) +) + +type imlSystemPermitController struct { + systemPermitModule system.ISystemPermitModule `autowired:""` +} + +func (c *imlSystemPermitController) Permissions(ctx *gin.Context) ([]string, error) { + return c.systemPermitModule.Permissions(ctx) +} + +func (c *imlSystemPermitController) OnComplete() { + +} diff --git a/controller/permit_system/permit.go b/controller/permit_system/permit.go new file mode 100644 index 00000000..28946145 --- /dev/null +++ b/controller/permit_system/permit.go @@ -0,0 +1,18 @@ +package permit_system + +import ( + "reflect" + + "github.com/eolinker/go-common/autowire" + "github.com/gin-gonic/gin" +) + +type ISystemPermitController interface { + Permissions(ctx *gin.Context) ([]string, error) +} + +func init() { + autowire.Auto[ISystemPermitController](func() reflect.Value { + return reflect.ValueOf(new(imlSystemPermitController)) + }) +} diff --git a/controller/permit_team/iml.go b/controller/permit_team/iml.go new file mode 100644 index 00000000..03bb8912 --- /dev/null +++ b/controller/permit_team/iml.go @@ -0,0 +1,18 @@ +package permit_team + +import ( + "github.com/APIParkLab/APIPark/module/permit/team" + "github.com/gin-gonic/gin" +) + +var ( + _ ITeamPermitController = (*imlTeamPermitController)(nil) +) + +type imlTeamPermitController struct { + teamPermitModule team.ITeamPermitModule `autowired:""` +} + +func (c *imlTeamPermitController) Permissions(ctx *gin.Context, team string) ([]string, error) { + return c.teamPermitModule.Permissions(ctx, team) +} diff --git a/controller/permit_team/permit.go b/controller/permit_team/permit.go new file mode 100644 index 00000000..c02de0d5 --- /dev/null +++ b/controller/permit_team/permit.go @@ -0,0 +1,18 @@ +package permit_team + +import ( + "reflect" + + "github.com/eolinker/go-common/autowire" + "github.com/gin-gonic/gin" +) + +type ITeamPermitController interface { + Permissions(ctx *gin.Context, team string) ([]string, error) +} + +func init() { + autowire.Auto[ITeamPermitController](func() reflect.Value { + return reflect.ValueOf(new(imlTeamPermitController)) + }) +} diff --git a/controller/plugin-cluster/iml.go b/controller/plugin-cluster/iml.go new file mode 100644 index 00000000..f4a1f541 --- /dev/null +++ b/controller/plugin-cluster/iml.go @@ -0,0 +1,36 @@ +package plugin_cluster + +import ( + "github.com/APIParkLab/APIPark/model/plugin_model" + "github.com/APIParkLab/APIPark/module/plugin-cluster" + "github.com/APIParkLab/APIPark/module/plugin-cluster/dto" + "github.com/gin-gonic/gin" +) + +var ( + _ IPluginClusterController = (*imlPluginClusterController)(nil) +) + +type imlPluginClusterController struct { + module plugin_cluster.IPluginClusterModule `autowired:""` +} + +func (i *imlPluginClusterController) Info(ctx *gin.Context, name string) (*dto.Define, error) { + return i.module.GetDefine(ctx, name) +} + +func (i *imlPluginClusterController) Option(ctx *gin.Context, project string) ([]*dto.PluginOption, error) { + return i.module.Options(ctx) +} + +func (i *imlPluginClusterController) List(ctx *gin.Context, clusterId string) ([]*dto.Item, error) { + return i.module.List(ctx, clusterId) +} + +func (i *imlPluginClusterController) Get(ctx *gin.Context, clusterId string, name string) (config *dto.PluginOutput, render plugin_model.Render, er error) { + return i.module.Get(ctx, clusterId, name) +} + +func (i *imlPluginClusterController) Set(ctx *gin.Context, clusterId string, name string, config *dto.PluginSetting) error { + return i.module.Set(ctx, clusterId, name, config) +} diff --git a/controller/plugin-cluster/plugin_partiton.go b/controller/plugin-cluster/plugin_partiton.go new file mode 100644 index 00000000..a61670a6 --- /dev/null +++ b/controller/plugin-cluster/plugin_partiton.go @@ -0,0 +1,24 @@ +package plugin_cluster + +import ( + "reflect" + + "github.com/APIParkLab/APIPark/model/plugin_model" + "github.com/APIParkLab/APIPark/module/plugin-cluster/dto" + "github.com/eolinker/go-common/autowire" + "github.com/gin-gonic/gin" +) + +type IPluginClusterController interface { + List(ctx *gin.Context, clusterId string) ([]*dto.Item, error) + Get(ctx *gin.Context, clusterId string, name string) (config *dto.PluginOutput, render plugin_model.Render, er error) + Set(ctx *gin.Context, clusterId string, name string, config *dto.PluginSetting) error + Option(ctx *gin.Context, project string) ([]*dto.PluginOption, error) + Info(ctx *gin.Context, name string) (*dto.Define, error) +} + +func init() { + autowire.Auto[IPluginClusterController](func() reflect.Value { + return reflect.ValueOf(new(imlPluginClusterController)) + }) +} diff --git a/controller/publish/iml.go b/controller/publish/iml.go new file mode 100644 index 00000000..3e14ddda --- /dev/null +++ b/controller/publish/iml.go @@ -0,0 +1,132 @@ +package publish + +import ( + "strconv" + + "github.com/APIParkLab/APIPark/module/publish" + "github.com/APIParkLab/APIPark/module/publish/dto" + "github.com/APIParkLab/APIPark/module/release" + dto2 "github.com/APIParkLab/APIPark/module/release/dto" + "github.com/gin-gonic/gin" +) + +var ( + _ IPublishController = (*imlPublishController)(nil) +) + +type imlPublishController struct { + publishModule publish.IPublishModule `autowired:""` + releaseModule release.IReleaseModule `autowired:""` +} + +func (c *imlPublishController) ReleaseDo(ctx *gin.Context, serviceId string, input *dto.ApplyOnReleaseInput) (*dto.Publish, error) { + newReleaseId, err := c.releaseModule.Create(ctx, serviceId, &dto2.CreateInput{ + Version: input.Version, + Remark: input.VersionRemark, + }) + if err != nil { + return nil, err + } + apply, err := c.publishModule.Apply(ctx, serviceId, &dto.ApplyInput{ + Release: newReleaseId, + Remark: input.PublishRemark, + }) + if err != nil { + return nil, err + } + err = c.publishModule.Accept(ctx, serviceId, apply.Id, "") + if err != nil { + c.releaseModule.Delete(ctx, serviceId, newReleaseId) + return nil, err + } + err = c.publishModule.Publish(ctx, serviceId, apply.Id) + if err != nil { + c.releaseModule.Delete(ctx, serviceId, newReleaseId) + return nil, err + } + err = c.publishModule.Publish(ctx, serviceId, apply.Id) + if err != nil { + c.releaseModule.Delete(ctx, serviceId, newReleaseId) + return nil, err + } + return apply, err +} + +func (c *imlPublishController) PublishStatuses(ctx *gin.Context, serviceId string, id string) ([]*dto.PublishStatus, error) { + return c.publishModule.PublishStatuses(ctx, serviceId, id) +} + +func (c *imlPublishController) ApplyOnRelease(ctx *gin.Context, serviceId string, input *dto.ApplyOnReleaseInput) (*dto.Publish, error) { + newReleaseId, err := c.releaseModule.Create(ctx, serviceId, &dto2.CreateInput{ + Version: input.Version, + Remark: input.VersionRemark, + }) + if err != nil { + return nil, err + } + apply, err := c.publishModule.Apply(ctx, serviceId, &dto.ApplyInput{ + Release: newReleaseId, + Remark: input.PublishRemark, + }) + if err != nil { + return nil, err + } + return apply, nil +} + +func (c *imlPublishController) Apply(ctx *gin.Context, serviceId string, input *dto.ApplyInput) (*dto.Publish, error) { + apply, err := c.publishModule.Apply(ctx, serviceId, input) + if err != nil { + return nil, err + } + return apply, nil +} + +func (c *imlPublishController) CheckPublish(ctx *gin.Context, serviceId string, releaseId string) (*dto.DiffOut, error) { + return c.publishModule.CheckPublish(ctx, serviceId, releaseId) +} + +func (c *imlPublishController) Close(ctx *gin.Context, serviceId string, id string) error { + err := c.publishModule.Stop(ctx, serviceId, id) + if err != nil { + return err + } + return nil +} + +func (c *imlPublishController) Stop(ctx *gin.Context, serviceId string, id string) error { + return c.publishModule.Stop(ctx, serviceId, id) +} + +func (c *imlPublishController) Refuse(ctx *gin.Context, serviceId string, id string, input *dto.Comments) error { + return c.publishModule.Refuse(ctx, serviceId, id, input.Comments) +} + +func (c *imlPublishController) Accept(ctx *gin.Context, serviceId string, id string, input *dto.Comments) error { + return c.publishModule.Accept(ctx, serviceId, id, input.Comments) +} + +func (c *imlPublishController) Publish(ctx *gin.Context, serviceId string, id string) error { + return c.publishModule.Publish(ctx, serviceId, id) +} + +func (c *imlPublishController) ListPage(ctx *gin.Context, serviceId string, page, pageSize string) ([]*dto.Publish, int, int, int64, error) { + pageNum, _ := strconv.Atoi(page) + pageSizeNum, _ := strconv.Atoi(pageSize) + if pageNum < 1 { + pageNum = 1 + } + if pageSizeNum <= 0 { + pageSizeNum = 50 + } + list, total, err := c.publishModule.List(ctx, serviceId, pageNum, pageSizeNum) + if err != nil { + return nil, 0, 0, 0, err + } + + return list, pageNum, pageSizeNum, total, nil +} + +func (c *imlPublishController) Detail(ctx *gin.Context, serviceId string, id string) (*dto.PublishDetail, error) { + return c.publishModule.Detail(ctx, serviceId, id) +} diff --git a/controller/publish/publish.go b/controller/publish/publish.go new file mode 100644 index 00000000..afe2eb55 --- /dev/null +++ b/controller/publish/publish.go @@ -0,0 +1,34 @@ +package publish + +import ( + "reflect" + + "github.com/APIParkLab/APIPark/module/publish/dto" + "github.com/eolinker/go-common/autowire" + "github.com/gin-gonic/gin" +) + +var ( + _ IPublishController = (*imlPublishController)(nil) +) + +type IPublishController interface { + CheckPublish(ctx *gin.Context, serviceId string, releaseId string) (*dto.DiffOut, error) + ReleaseDo(ctx *gin.Context, serviceId string, input *dto.ApplyOnReleaseInput) (*dto.Publish, error) + ApplyOnRelease(ctx *gin.Context, serviceId string, input *dto.ApplyOnReleaseInput) (*dto.Publish, error) + Apply(ctx *gin.Context, serviceId string, input *dto.ApplyInput) (*dto.Publish, error) + Close(ctx *gin.Context, serviceId string, id string) error + Stop(ctx *gin.Context, serviceId string, id string) error + Refuse(ctx *gin.Context, serviceId string, id string, input *dto.Comments) error + Accept(ctx *gin.Context, serviceId string, id string, input *dto.Comments) error + Publish(ctx *gin.Context, serviceId string, id string) error + ListPage(ctx *gin.Context, serviceId string, page, pageSize string) ([]*dto.Publish, int, int, int64, error) + Detail(ctx *gin.Context, serviceId string, id string) (*dto.PublishDetail, error) + PublishStatuses(ctx *gin.Context, serviceId string, id string) ([]*dto.PublishStatus, error) +} + +func init() { + autowire.Auto[IPublishController](func() reflect.Value { + return reflect.ValueOf(new(imlPublishController)) + }) +} diff --git a/controller/release/iml.go b/controller/release/iml.go new file mode 100644 index 00000000..d473f943 --- /dev/null +++ b/controller/release/iml.go @@ -0,0 +1,44 @@ +package release + +import ( + "github.com/APIParkLab/APIPark/module/release" + "github.com/APIParkLab/APIPark/module/release/dto" + service_diff "github.com/APIParkLab/APIPark/module/service-diff" + "github.com/gin-gonic/gin" +) + +var ( + _ IReleaseController = (*imlReleaseController)(nil) +) + +type imlReleaseController struct { + module release.IReleaseModule `autowired:""` + diffModule service_diff.IServiceDiffModule `autowired:""` +} + +func (c *imlReleaseController) Create(ctx *gin.Context, project string, input *dto.CreateInput) error { + + _, err := c.module.Create(ctx, project, input) + return err +} +func (c *imlReleaseController) Delete(ctx *gin.Context, project string, id string) error { + return c.module.Delete(ctx, project, id) +} +func (c *imlReleaseController) Detail(ctx *gin.Context, project string, id string) (*dto.Detail, error) { + return c.module.Detail(ctx, project, id) +} +func (c *imlReleaseController) List(ctx *gin.Context, project string) ([]*dto.Release, error) { + return c.module.List(ctx, project) +} +func (c *imlReleaseController) Preview(ctx *gin.Context, project string) (*dto.Release, *service_diff.DiffOut, bool, error) { + releaseInfo, diff, complete, err := c.module.Preview(ctx, project) + if err != nil { + return nil, nil, false, err + } + + out, err := c.diffModule.Out(ctx, diff) + if err != nil { + return nil, nil, false, err + } + return releaseInfo, out, complete, nil +} diff --git a/controller/release/release.go b/controller/release/release.go new file mode 100644 index 00000000..1682014e --- /dev/null +++ b/controller/release/release.go @@ -0,0 +1,25 @@ +package release + +import ( + "reflect" + + service_diff "github.com/APIParkLab/APIPark/module/service-diff" + + "github.com/APIParkLab/APIPark/module/release/dto" + "github.com/eolinker/go-common/autowire" + "github.com/gin-gonic/gin" +) + +type IReleaseController interface { + Create(ctx *gin.Context, project string, input *dto.CreateInput) error + Delete(ctx *gin.Context, project string, id string) error + Detail(ctx *gin.Context, project string, id string) (*dto.Detail, error) + List(ctx *gin.Context, project string) ([]*dto.Release, error) + Preview(ctx *gin.Context, project string) (*dto.Release, *service_diff.DiffOut, bool, error) +} + +func init() { + autowire.Auto[IReleaseController](func() reflect.Value { + return reflect.ValueOf(new(imlReleaseController)) + }) +} diff --git a/controller/service/iml.go b/controller/service/iml.go new file mode 100644 index 00000000..f4b31e47 --- /dev/null +++ b/controller/service/iml.go @@ -0,0 +1,91 @@ +package service + +import ( + "github.com/APIParkLab/APIPark/module/service" + service_dto "github.com/APIParkLab/APIPark/module/service/dto" + "github.com/gin-gonic/gin" +) + +var ( + _ IServiceController = (*imlServiceController)(nil) + _ IAppController = (*imlAppController)(nil) +) + +type imlServiceController struct { + module service.IServiceModule `autowired:""` +} + +func (i *imlServiceController) SearchMyServices(ctx *gin.Context, teamId string, keyword string) ([]*service_dto.ServiceItem, error) { + return i.module.SearchMyServices(ctx, teamId, keyword) +} + +func (i *imlServiceController) Simple(ctx *gin.Context, keyword string) ([]*service_dto.SimpleServiceItem, error) { + return i.module.Simple(ctx, keyword) +} + +func (i *imlServiceController) MySimple(ctx *gin.Context, keyword string) ([]*service_dto.SimpleServiceItem, error) { + return i.module.MySimple(ctx, keyword) +} + +func (i *imlServiceController) Get(ctx *gin.Context, id string) (*service_dto.Service, error) { + return i.module.Get(ctx, id) +} + +func (i *imlServiceController) Search(ctx *gin.Context, teamID string, keyword string) ([]*service_dto.ServiceItem, error) { + return i.module.Search(ctx, teamID, keyword) +} + +func (i *imlServiceController) Create(ctx *gin.Context, teamID string, input *service_dto.CreateService) (*service_dto.Service, error) { + return i.module.Create(ctx, teamID, input) +} + +func (i *imlServiceController) Edit(ctx *gin.Context, id string, input *service_dto.EditService) (*service_dto.Service, error) { + return i.module.Edit(ctx, id, input) +} + +func (i *imlServiceController) Delete(ctx *gin.Context, id string) error { + return i.module.Delete(ctx, id) +} + +func (i *imlServiceController) ServiceDoc(ctx *gin.Context, id string) (*service_dto.ServiceDoc, error) { + return i.module.ServiceDoc(ctx, id) +} + +func (i *imlServiceController) SaveServiceDoc(ctx *gin.Context, id string, input *service_dto.SaveServiceDoc) error { + return i.module.SaveServiceDoc(ctx, id, input) +} + +type imlAppController struct { + module service.IAppModule `autowired:""` +} + +func (i *imlAppController) Search(ctx *gin.Context, teamId string, keyword string) ([]*service_dto.AppItem, error) { + return i.module.Search(ctx, teamId, keyword) +} + +func (i *imlAppController) CreateApp(ctx *gin.Context, teamID string, input *service_dto.CreateApp) (*service_dto.App, error) { + return i.module.CreateApp(ctx, teamID, input) +} +func (i *imlAppController) UpdateApp(ctx *gin.Context, appId string, input *service_dto.UpdateApp) (*service_dto.App, error) { + return i.module.UpdateApp(ctx, appId, input) +} + +func (i *imlAppController) SearchMyApps(ctx *gin.Context, teamId string, keyword string) ([]*service_dto.AppItem, error) { + return i.module.SearchMyApps(ctx, teamId, keyword) +} + +func (i *imlAppController) SimpleApps(ctx *gin.Context, keyword string) ([]*service_dto.SimpleAppItem, error) { + return i.module.SimpleApps(ctx, keyword) +} + +func (i *imlAppController) MySimpleApps(ctx *gin.Context, keyword string) ([]*service_dto.SimpleAppItem, error) { + return i.module.MySimpleApps(ctx, keyword) +} + +func (i *imlAppController) GetApp(ctx *gin.Context, appId string) (*service_dto.App, error) { + return i.module.GetApp(ctx, appId) +} + +func (i *imlAppController) DeleteApp(ctx *gin.Context, appId string) error { + return i.module.DeleteApp(ctx, appId) +} diff --git a/controller/service/service.go b/controller/service/service.go new file mode 100644 index 00000000..2f17144b --- /dev/null +++ b/controller/service/service.go @@ -0,0 +1,55 @@ +package service + +import ( + "reflect" + + service_dto "github.com/APIParkLab/APIPark/module/service/dto" + + "github.com/gin-gonic/gin" + + "github.com/eolinker/go-common/autowire" +) + +type IServiceController interface { + // Get 获取 + Get(ctx *gin.Context, id string) (*service_dto.Service, error) + // SearchMyServices 搜索服务 + SearchMyServices(ctx *gin.Context, teamID string, keyword string) ([]*service_dto.ServiceItem, error) + Search(ctx *gin.Context, teamID string, keyword string) ([]*service_dto.ServiceItem, error) + // Create 创建 + Create(ctx *gin.Context, teamID string, input *service_dto.CreateService) (*service_dto.Service, error) + // Edit 编辑 + Edit(ctx *gin.Context, id string, input *service_dto.EditService) (*service_dto.Service, error) + // Delete 删除 + Delete(ctx *gin.Context, id string) error + // Simple 获取简易列表 + Simple(ctx *gin.Context, keyword string) ([]*service_dto.SimpleServiceItem, error) + // MySimple 获取我的简易列表 + MySimple(ctx *gin.Context, keyword string) ([]*service_dto.SimpleServiceItem, error) + ServiceDoc(ctx *gin.Context, id string) (*service_dto.ServiceDoc, error) + SaveServiceDoc(ctx *gin.Context, id string, input *service_dto.SaveServiceDoc) error +} + +type IAppController interface { + // CreateApp 创建应用 + CreateApp(ctx *gin.Context, teamID string, project *service_dto.CreateApp) (*service_dto.App, error) + + UpdateApp(ctx *gin.Context, appId string, project *service_dto.UpdateApp) (*service_dto.App, error) + Search(ctx *gin.Context, teamId string, keyword string) ([]*service_dto.AppItem, error) + SearchMyApps(ctx *gin.Context, teamId string, keyword string) ([]*service_dto.AppItem, error) + // SimpleApps 获取简易项目列表 + SimpleApps(ctx *gin.Context, keyword string) ([]*service_dto.SimpleAppItem, error) + MySimpleApps(ctx *gin.Context, keyword string) ([]*service_dto.SimpleAppItem, error) + GetApp(ctx *gin.Context, appId string) (*service_dto.App, error) + DeleteApp(ctx *gin.Context, appId string) error +} + +func init() { + autowire.Auto[IServiceController](func() reflect.Value { + return reflect.ValueOf(new(imlServiceController)) + }) + + autowire.Auto[IAppController](func() reflect.Value { + return reflect.ValueOf(new(imlAppController)) + }) +} diff --git a/controller/subscribe/iml.go b/controller/subscribe/iml.go new file mode 100644 index 00000000..9557d7a2 --- /dev/null +++ b/controller/subscribe/iml.go @@ -0,0 +1,74 @@ +package subscribe + +import ( + "fmt" + + "github.com/gin-gonic/gin" + + "github.com/APIParkLab/APIPark/module/subscribe" + subscribe_dto "github.com/APIParkLab/APIPark/module/subscribe/dto" +) + +var ( + _ ISubscribeController = (*imlSubscribeController)(nil) +) + +type imlSubscribeController struct { + module subscribe.ISubscribeModule `autowired:""` +} + +//func (i *imlSubscribeController) PartitionServices(ctx *gin.Context, app string) ([]*subscribe_dto.PartitionServiceItem, error) { +// return i.module.PartitionServices(ctx, app) +//} + +func (i *imlSubscribeController) SearchSubscriptions(ctx *gin.Context, appId string, keyword string) ([]*subscribe_dto.SubscriptionItem, error) { + return i.module.SearchSubscriptions(ctx, appId, keyword) +} + +func (i *imlSubscribeController) RevokeSubscription(ctx *gin.Context, service string, uuid string) error { + return i.module.RevokeSubscription(ctx, service, uuid) +} + +func (i *imlSubscribeController) DeleteSubscription(ctx *gin.Context, service string, uuid string) error { + return i.module.DeleteSubscription(ctx, service, uuid) +} + +func (i *imlSubscribeController) AddSubscriber(ctx *gin.Context, service string, input *subscribe_dto.AddSubscriber) error { + return i.module.AddSubscriber(ctx, service, input) +} + +func (i *imlSubscribeController) DeleteSubscriber(ctx *gin.Context, service string, serviceId string, applicationId string) error { + return i.module.DeleteSubscriber(ctx, service, serviceId, applicationId) +} + +func (i *imlSubscribeController) RevokeApply(ctx *gin.Context, service string, uuid string) error { + return i.module.RevokeApply(ctx, service, uuid) +} + +func (i *imlSubscribeController) Search(ctx *gin.Context, service string, keyword string) ([]*subscribe_dto.Subscriber, error) { + return i.module.SearchSubscribers(ctx, service, keyword) +} + +var _ ISubscribeApprovalController = (*imlSubscribeApprovalController)(nil) + +type imlSubscribeApprovalController struct { + module subscribe.ISubscribeApprovalModule `autowired:""` +} + +func (i *imlSubscribeApprovalController) GetApprovalList(ctx *gin.Context, service string, status int) ([]*subscribe_dto.ApprovalItem, error) { + return i.module.GetApprovalList(ctx, service, status) +} + +func (i *imlSubscribeApprovalController) GetApprovalDetail(ctx *gin.Context, service string, id string) (*subscribe_dto.Approval, error) { + return i.module.GetApprovalDetail(ctx, service, id) +} + +func (i *imlSubscribeApprovalController) Approval(ctx *gin.Context, service string, id string, approveInfo *subscribe_dto.Approve) error { + switch approveInfo.Operate { + case "pass": + return i.module.Pass(ctx, service, id, approveInfo) + case "refuse": + return i.module.Reject(ctx, service, id, approveInfo) + } + return fmt.Errorf("unknown operate: %s", approveInfo.Operate) +} diff --git a/controller/subscribe/subscribe.go b/controller/subscribe/subscribe.go new file mode 100644 index 00000000..2c85a6f4 --- /dev/null +++ b/controller/subscribe/subscribe.go @@ -0,0 +1,47 @@ +package subscribe + +import ( + "reflect" + + "github.com/gin-gonic/gin" + + subscribe_dto "github.com/APIParkLab/APIPark/module/subscribe/dto" + + "github.com/eolinker/go-common/autowire" +) + +type ISubscribeController interface { + // AddSubscriber 添加订阅者 + AddSubscriber(ctx *gin.Context, project string, input *subscribe_dto.AddSubscriber) error + // DeleteSubscriber 删除订阅者 + DeleteSubscriber(ctx *gin.Context, project string, serviceId string, applicationId string) error + // Search 关键字获取订阅者列表 + Search(ctx *gin.Context, project string, keyword string) ([]*subscribe_dto.Subscriber, error) + // SearchSubscriptions 关键字获取订阅服务列表 + SearchSubscriptions(ctx *gin.Context, appId string, keyword string) ([]*subscribe_dto.SubscriptionItem, error) + // RevokeSubscription 取消订阅 + RevokeSubscription(ctx *gin.Context, project string, uuid string) error + // DeleteSubscription 删除订阅 + DeleteSubscription(ctx *gin.Context, project string, uuid string) error + // RevokeApply 取消申请 + RevokeApply(ctx *gin.Context, project string, uuid string) error + //PartitionServices(ctx *gin.Context, app string) ([]*subscribe_dto.PartitionServiceItem, error) +} + +type ISubscribeApprovalController interface { + // GetApprovalList 获取审批列表 + GetApprovalList(ctx *gin.Context, project string, status int) ([]*subscribe_dto.ApprovalItem, error) + // GetApprovalDetail 获取审批详情 + GetApprovalDetail(ctx *gin.Context, project string, id string) (*subscribe_dto.Approval, error) + // Approval 审批 + Approval(ctx *gin.Context, project string, id string, approveInfo *subscribe_dto.Approve) error +} + +func init() { + autowire.Auto[ISubscribeController](func() reflect.Value { + return reflect.ValueOf(new(imlSubscribeController)) + }) + autowire.Auto[ISubscribeApprovalController](func() reflect.Value { + return reflect.ValueOf(new(imlSubscribeApprovalController)) + }) +} diff --git a/controller/team_manager/iml.go b/controller/team_manager/iml.go new file mode 100644 index 00000000..0053e711 --- /dev/null +++ b/controller/team_manager/iml.go @@ -0,0 +1,39 @@ +package team_manager + +import ( + "github.com/APIParkLab/APIPark/module/team" + team_dto "github.com/APIParkLab/APIPark/module/team/dto" + "github.com/gin-gonic/gin" +) + +var ( + _ ITeamManagerController = (*imlTeamManagerController)(nil) +) + +type imlTeamManagerController struct { + module team.ITeamModule `autowired:""` +} + +func (c *imlTeamManagerController) GetTeam(ctx *gin.Context, id string) (*team_dto.Team, error) { + return c.module.GetTeam(ctx, id) +} + +func (c *imlTeamManagerController) Search(ctx *gin.Context, keyword string) ([]*team_dto.Item, error) { + return c.module.Search(ctx, keyword) +} + +func (c *imlTeamManagerController) CreateTeam(ctx *gin.Context, team *team_dto.CreateTeam) (*team_dto.Team, error) { + return c.module.Create(ctx, team) +} + +func (c *imlTeamManagerController) EditTeam(ctx *gin.Context, id string, team *team_dto.EditTeam) (*team_dto.Team, error) { + return c.module.Edit(ctx, id, team) +} + +func (c *imlTeamManagerController) DeleteTeam(ctx *gin.Context, id string) (string, error) { + err := c.module.Delete(ctx, id) + if err != nil { + return "", err + } + return id, nil +} diff --git a/controller/team_manager/team.go b/controller/team_manager/team.go new file mode 100644 index 00000000..5e85164e --- /dev/null +++ b/controller/team_manager/team.go @@ -0,0 +1,23 @@ +package team_manager + +import ( + team_dto "github.com/APIParkLab/APIPark/module/team/dto" + "github.com/eolinker/go-common/autowire" + "github.com/gin-gonic/gin" + "reflect" +) + +type ITeamManagerController interface { + // GetTeam 获取团队信息 + GetTeam(ctx *gin.Context, id string) (*team_dto.Team, error) + Search(ctx *gin.Context, keyword string) ([]*team_dto.Item, error) + CreateTeam(ctx *gin.Context, team *team_dto.CreateTeam) (*team_dto.Team, error) + EditTeam(ctx *gin.Context, id string, team *team_dto.EditTeam) (*team_dto.Team, error) + DeleteTeam(ctx *gin.Context, id string) (string, error) +} + +func init() { + autowire.Auto[ITeamManagerController](func() reflect.Value { + return reflect.ValueOf(new(imlTeamManagerController)) + }) +} diff --git a/controller/upstream/iml.go b/controller/upstream/iml.go new file mode 100644 index 00000000..ac16a361 --- /dev/null +++ b/controller/upstream/iml.go @@ -0,0 +1,27 @@ +package upstream + +import ( + "github.com/APIParkLab/APIPark/module/cluster" + "github.com/APIParkLab/APIPark/module/service" + "github.com/APIParkLab/APIPark/module/upstream" + upstream_dto "github.com/APIParkLab/APIPark/module/upstream/dto" + "github.com/gin-gonic/gin" +) + +var ( + _ IUpstreamController = (*imlUpstreamController)(nil) +) + +type imlUpstreamController struct { + upstreamModule upstream.IUpstreamModule `autowired:""` + projectModule service.IServiceModule `autowired:""` + partitionModule cluster.IClusterModule `autowired:""` +} + +func (i *imlUpstreamController) Get(ctx *gin.Context, serviceId string) (upstream_dto.UpstreamConfig, error) { + return i.upstreamModule.Get(ctx, serviceId) +} + +func (i *imlUpstreamController) Save(ctx *gin.Context, serviceId string, upstream *upstream_dto.UpstreamConfig) (upstream_dto.UpstreamConfig, error) { + return i.upstreamModule.Save(ctx, serviceId, *upstream) +} diff --git a/controller/upstream/upstream.go b/controller/upstream/upstream.go new file mode 100644 index 00000000..10ce9698 --- /dev/null +++ b/controller/upstream/upstream.go @@ -0,0 +1,21 @@ +package upstream + +import ( + "reflect" + + "github.com/eolinker/go-common/autowire" + + upstream_dto "github.com/APIParkLab/APIPark/module/upstream/dto" + "github.com/gin-gonic/gin" +) + +type IUpstreamController interface { + Get(ctx *gin.Context, serviceId string) (upstream_dto.UpstreamConfig, error) + Save(ctx *gin.Context, serviceId string, upstream *upstream_dto.UpstreamConfig) (upstream_dto.UpstreamConfig, error) +} + +func init() { + autowire.Auto[IUpstreamController](func() reflect.Value { + return reflect.ValueOf(new(imlUpstreamController)) + }) +} diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 00000000..2196ff5f --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +market_dist +tenant_dist +*/.clinic +dist-ssr +*.local +packages/core/public/tinymce/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +/pnpm-lock.yaml diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 00000000..68a7459a --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,16 @@ + +# 部署 + +## 安装依赖 + 建议使用pnpm + `npm install -g pnpm` + 使用pnpm安装依赖 + `pnpm install` + +## 编译 + `pnpm run build` \ No newline at end of file diff --git a/frontend/README.pro.md b/frontend/README.pro.md new file mode 100644 index 00000000..93bfd817 --- /dev/null +++ b/frontend/README.pro.md @@ -0,0 +1,17 @@ +# 部署 + +## 代码同步 + packages目录下,部分子项目为企业版独有,不要同步到开源版: + packages/businessEntry, packages/dashboard, packages/openApi, packages/systemRunning, README.pro.md + +## 安装依赖 + 建议使用pnpm + `npm install -g pnpm` + 使用pnpm安装依赖 + `pnpm install` + +## 编译 +### 开源版本 + `pnpm run build` +### 企业版本 + `pnpm run build:pro` \ No newline at end of file diff --git a/frontend/frontend.go b/frontend/frontend.go new file mode 100644 index 00000000..96f8a084 --- /dev/null +++ b/frontend/frontend.go @@ -0,0 +1,108 @@ +package frontend + +import ( + "embed" + _ "embed" + "fmt" + "io/fs" + "net/http" + "path" + "strings" + "time" + + "github.com/eolinker/go-common/pm3" + "github.com/eolinker/go-common/server" + "github.com/gabriel-vasile/mimetype" + "github.com/gin-gonic/gin" +) + +var ( + //go:embed dist/favicon.ico + iconContent []byte + iconType string + //go:embed dist/vite.svg + viteContent []byte + viteContentType string + //go:embed dist + dist embed.FS + //go:embed dist/index.html + indexHtml []byte +) +var ( + expires = time.Hour * 24 * 7 + cacheControl = fmt.Sprintf("public, max-age=%d", 3600*24*7) +) + +func AddExpires(ginCtx *gin.Context) { + ginCtx.Header("Expires", time.Now().Add(expires).UTC().Format(http.TimeFormat)) + ginCtx.Header("Cache-Control", cacheControl) +} + +func init() { + iconType = mimetype.Detect(iconContent).String() + viteContentType = mimetype.Detect(viteContent).String() + server.SetIndexHtmlHandler(IndexHtml) + server.AddSystemPlugin(new(Frontend)) +} +func getFileSystem(dir string) http.FileSystem { + fDir, err := fs.Sub(dist, path.Join("dist", dir)) + if err != nil { + panic(err) + } + + return http.FS(fDir) + +} + +type Frontend struct { +} + +func (f *Frontend) Middlewares() []pm3.IMiddleware { + return []pm3.IMiddleware{ + pm3.CreateMiddle(func(method, path string) bool { + if method != http.MethodGet { + return false + } + if strings.HasPrefix(path, "/api") { + return false + } + return true + }, AddExpires, 0), + } +} + +func (f *Frontend) Name() string { + return "baseFrontend" +} + +func IndexHtml(ginCtx *gin.Context) { + AddExpires(ginCtx) + ginCtx.Header("Cache-Control", "no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate") + ginCtx.Data(http.StatusOK, "text/html; charset=utf-8", indexHtml) +} + +func (f *Frontend) Api() []pm3.Api { + return []pm3.Api{ + pm3.CreateApiSimple(http.MethodGet, "/favicon.ico", func(ginCtx *gin.Context) { + ginCtx.Data(http.StatusOK, iconType, iconContent) + }), + pm3.CreateApiSimple(http.MethodGet, "/vite.svg", func(ginCtx *gin.Context) { + ginCtx.Data(http.StatusOK, viteContentType, viteContent) + }), + } +} +func (f *Frontend) Files() []pm3.FrontendFiles { + return []pm3.FrontendFiles{ + + { + Path: "/assets/", + FileSystem: getFileSystem("assets"), + }, { + Path: "/tinymce/", + FileSystem: getFileSystem("tinymce"), + }, { + Path: "/frontend/", + FileSystem: getFileSystem("/"), + }, + } +} diff --git a/frontend/jest.config.js b/frontend/jest.config.js new file mode 100644 index 00000000..8d74aab3 --- /dev/null +++ b/frontend/jest.config.js @@ -0,0 +1,18 @@ +/* + * @Date: 2024-05-10 14:19:56 + * @LastEditors: maggieyyy + * @LastEditTime: 2024-05-10 15:55:29 + * @FilePath: \frontend\jest.config.js + */ + +module.exports = { + roots: ['/packages'], + testMatch: ['**/__tests__/**/*.+(ts|tsx|js)', '**/?(*.)+(spec|test).+(ts|tsx|js)'], + transform: { + '^.+\\.(ts|tsx)$': 'ts-jest', + }, + testEnvironment: 'jsdom', + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], + testPathIgnorePatterns: ['/node_modules/', '/dist/'], + setupFilesAfterEnv: ['/jest.setup.js'], +}; \ No newline at end of file diff --git a/frontend/jest.setup.js b/frontend/jest.setup.js new file mode 100644 index 00000000..4fb49c35 --- /dev/null +++ b/frontend/jest.setup.js @@ -0,0 +1,7 @@ +/* + * @Date: 2024-05-10 14:22:41 + * @LastEditors: maggieyyy + * @LastEditTime: 2024-05-10 15:49:31 + * @FilePath: \frontend\jest.setup.js + */ +// import '@testing-library/jest-dom/extend-expect'; \ No newline at end of file diff --git a/frontend/lerna.json b/frontend/lerna.json new file mode 100644 index 00000000..558e6109 --- /dev/null +++ b/frontend/lerna.json @@ -0,0 +1,6 @@ +{ + "packages": [ + "packages/*" + ], + "version": "independent" +} \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 00000000..bcd484ac --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,81 @@ +{ + "name": "frontend", + "version": "1.0.0", + "private": true, + "workspaces": [ + "packages/*" + ], + "description": "", + "scripts": { + "test": "jest", + "build": "set NODE_OPTIONS=--max-old-space-size=4096 && lerna run build --scope=core --stream --verbose ", + "build:pro": "set NODE_OPTIONS=--max-old-space-size=4096 && lerna run build --scope=business-entry --stream --verbose ", + "serve": "lerna run preview --parallel", + "serve:remotes": "lerna run serve --scope=remote --parallel", + "dev": "lerna run dev --scope=core --stream", + "dev:pro": "lerna run dev --scope=business-entry --stream", + "stop": "kill-port --port 5000,5001" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@ant-design/icons": "^5.2.6", + "@ant-design/pro-components": "2.7.9", + "@originjs/vite-plugin-federation": "^1.3.3", + "@rollup/plugin-dynamic-import-vars": "^2.1.2", + "@types/lodash-es": "^4.17.12", + "@types/uuid": "^9.0.7", + "@vitejs/plugin-react": "^4.2.0", + "autoprefixer": "^10.4.16", + "dayjs": "^1.11.10", + "js-base64": "^3.7.5", + "moment": "^2.29.4", + "postcss": "^8.4.31", + "postcss-import": "^16.1.0", + "postcss-nesting": "^12.1.5", + "react": "^18.2.0", + "react-ace": "^10.1.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.20.0", + "tailwindcss": "^3.3.5", + "uuid": "^9.0.1", + "vite-tsconfig-paths": "^4.3.2" + }, + "devDependencies": { + "@ant-design/cssinjs": "^1.18.2", + "@antv/g6": "^4.8.24", + "@iconify/react": "^5.0.2", + "@testing-library/jest-dom": "^6.4.5", + "@testing-library/react": "^15.0.7", + "@testing-library/react-hooks": "^8.0.1", + "@types/file-saver": "^2.0.7", + "@types/jest": "^29.5.12", + "@types/node": "^20.10.5", + "@types/react": "^18.2.37", + "@types/react-dom": "^18.2.15", + "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^6.10.0", + "@vitejs/plugin-react": "^4.2.0", + "antd": "^5.19.4", + "babel-jest": "^29.7.0", + "eslint": "^8.53.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.4", + "file-saver": "^2.0.5", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "jest-fetch-mock": "^3.0.3", + "jsdom": "^24.0.0", + "lerna": "^8.1.3", + "less": "^4.2.0", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "postcss-nested": "^6.0.1", + "react-test-renderer": "^18.3.1", + "ts-jest": "^29.1.2", + "typescript": "^5.2.2", + "vite": "^5.0.0", + "vite-jest": "^0.1.4" + } +} diff --git a/frontend/packages/businessEntry/.env b/frontend/packages/businessEntry/.env new file mode 100644 index 00000000..ef729214 --- /dev/null +++ b/frontend/packages/businessEntry/.env @@ -0,0 +1,5 @@ + +// .env.pro +VITE_APP_MODE=pro +VITE_APP_TITLE=My Production App +VITE_API_BASE_URL=https://api.production.example.com \ No newline at end of file diff --git a/frontend/packages/businessEntry/.eslintrc.cjs b/frontend/packages/businessEntry/.eslintrc.cjs new file mode 100644 index 00000000..87e6dac6 --- /dev/null +++ b/frontend/packages/businessEntry/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs','public','code-snippet','ace-editor'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/frontend/packages/businessEntry/.gitignore b/frontend/packages/businessEntry/.gitignore new file mode 100644 index 00000000..c5647721 --- /dev/null +++ b/frontend/packages/businessEntry/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +public/tinymce + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + diff --git a/frontend/packages/businessEntry/README.md b/frontend/packages/businessEntry/README.md new file mode 100644 index 00000000..38866cff --- /dev/null +++ b/frontend/packages/businessEntry/README.md @@ -0,0 +1,11 @@ +# `businessEntry` + +> TODO: description + +## Usage + +``` +const businessEntry = require('businessEntry'); + +// TODO: DEMONSTRATE API +``` diff --git a/frontend/packages/businessEntry/__tests__/businessEntry.test.js b/frontend/packages/businessEntry/__tests__/businessEntry.test.js new file mode 100644 index 00000000..46213a5b --- /dev/null +++ b/frontend/packages/businessEntry/__tests__/businessEntry.test.js @@ -0,0 +1,7 @@ +'use strict'; + +const businessEntry = require('..'); +const assert = require('assert').strict; + +assert.strictEqual(businessEntry(), 'Hello from businessEntry'); +console.info('businessEntry tests passed'); diff --git a/frontend/packages/businessEntry/index.html b/frontend/packages/businessEntry/index.html new file mode 100644 index 00000000..3a72b495 --- /dev/null +++ b/frontend/packages/businessEntry/index.html @@ -0,0 +1,15 @@ + + + + + + + APIPark - 企业API数据开放平台 + + +
+ + + + + diff --git a/frontend/packages/businessEntry/package.json b/frontend/packages/businessEntry/package.json new file mode 100644 index 00000000..9db872f3 --- /dev/null +++ b/frontend/packages/businessEntry/package.json @@ -0,0 +1,17 @@ +{ + "name": "business-entry", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": " vite --port 5000 --strictPort", + "build": "vite build ", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview --port 5000 --strictPort", + "serve": "vite preview --port 5000 --strictPort" + }, + "dependencies": { + }, + "devDependencies": { + } +} diff --git a/frontend/packages/businessEntry/postcss.config.js b/frontend/packages/businessEntry/postcss.config.js new file mode 100644 index 00000000..69215941 --- /dev/null +++ b/frontend/packages/businessEntry/postcss.config.js @@ -0,0 +1,15 @@ +/* + * @Date: 2023-11-27 17:31:54 + * @LastEditors: maggieyyy + * @LastEditTime: 2024-06-05 10:42:18 + * @FilePath: \frontend\packages\core\postcss.config.js + */ +export default { + plugins: { + 'postcss-import': {}, + 'tailwindcss/nesting': {}, + tailwindcss: {}, + autoprefixer: {} + }, + } + \ No newline at end of file diff --git a/frontend/packages/businessEntry/public/favicon.ico b/frontend/packages/businessEntry/public/favicon.ico new file mode 100644 index 00000000..2c1de84c Binary files /dev/null and b/frontend/packages/businessEntry/public/favicon.ico differ diff --git a/frontend/packages/businessEntry/public/iconpark_apinto.js b/frontend/packages/businessEntry/public/iconpark_apinto.js new file mode 100644 index 00000000..d125a4a0 --- /dev/null +++ b/frontend/packages/businessEntry/public/iconpark_apinto.js @@ -0,0 +1,8 @@ +/* + * @Date: 2024-05-06 09:47:27 + * @LastEditors: maggieyyy + * @LastEditTime: 2024-05-06 09:47:47 + * @FilePath: \frontend\packages\core\src\assets\iconpark_apinto.js + */ +(function(){window.__iconpark__=window.__iconpark__||{};var obj=JSON.parse("{\"680840\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"none\",\"content\":\"\"},\"680856\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680857\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680858\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680859\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680860\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680861\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680862\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680863\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680864\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680865\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680866\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680867\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680868\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680869\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680870\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680871\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680872\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680873\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680874\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680875\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680876\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680877\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680878\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680879\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680880\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680881\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680882\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680883\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680884\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680885\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680886\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680887\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680888\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680889\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680890\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680891\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680892\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680893\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680894\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680895\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680896\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680897\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680898\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680899\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680900\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680901\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680902\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680903\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680904\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680905\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680906\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680907\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680908\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680909\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680910\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680911\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680912\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680913\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680914\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680915\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680916\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680917\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680918\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680919\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680920\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680921\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680922\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680923\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680924\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680925\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680926\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680927\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680928\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680929\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680930\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680931\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680932\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680933\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680934\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680935\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680936\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680937\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680938\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680939\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"681014\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"681015\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"681016\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"681017\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"681018\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"681019\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"681787\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"none\",\"content\":\"\"},\"694557\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"694558\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"707431\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"707736\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"707739\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"707741\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"707742\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"707743\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"707744\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"707749\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"708142\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"708144\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"708145\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"708146\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"708147\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"708181\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"709715\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"808898\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"808900\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"808916\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"810363\":{\"viewBox\":\"0 0 20 20\",\"fill\":\"none\",\"content\":\"\"},\"810396\":{\"viewBox\":\"0 0 20 20\",\"fill\":\"none\",\"content\":\"\"},\"818250\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"818340\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"none\",\"content\":\"\"}}");for(var _k in obj){window.__iconpark__[_k] = obj[_k]};var nm={"fuzhi1":680840,"bianji":680856,"tishi":680857,"xinkaibiaoqian":680858,"bianjiyoujian":680859,"suoyou":680860,"fuzhi":680861,"shijian":680862,"gundongxuanze":680863,"guanbi":680864,"zanting":680865,"chenggong":680866,"chakanyinyong":680867,"guidang":680868,"tihuan":680869,"zhankai":680870,"duankailianjie":680871,"Cookieguanli":680872,"riqi":680873,"zhongwen":680874,"yingwen":680875,"shuaxinjiankongzhuangtai":680876,"rili":680877,"genmulu":680878,"shangxianguanli":680879,"daimashili":680880,"yingyong":680881,"tianjia-2":680882,"qingchu":680883,"daoru":680884,"xiala":680885,"kuaisuceshi-2":680886,"saoyisao":680887,"shiyongjiaocheng":680888,"tianjiafujian":680889,"xiazai":680890,"jinzhidengji":680891,"fasongyoujian":680892,"bug":680893,"chaping":680894,"kuaisuceshi":680895,"yunshangchuan":680896,"yunxiazai":680897,"sousuo":680898,"peizhi":680899,"xinchuangkoudakai":680900,"shanchu-2":680901,"quanjusuoxiao":680902,"qiehuan":680903,"jianshao":680904,"chakanAPIlishi":680905,"lianjie":680906,"dunpaibaoxianrenzheng":680907,"shaixuan":680908,"zhihang":680909,"congmobanzhongchuangjianyongli":680910,"zhengligeshi":680911,"haoping":680912,"shouqi-2":680913,"gouwuche":680914,"lishi":680915,"tianjiaziji":680916,"tianjia":680917,"shouqi":680918,"gailan":680919,"paixu":680920,"gengduo":680921,"guanliyuanrenzheng":680922,"fenxiang":680923,"shanchu":680924,"yidong":680925,"chakan":680926,"shujuku":680927,"shangyoufuwu-":680928,"huanyuangeshi":680929,"jiangxu":680930,"shengxu":680931,"quanjufangda":680932,"xuanzhong":680933,"riqiqujian":680934,"ditu-1":680935,"qunzu":680936,"daochu":680937,"zhankai-":680938,"ditu-2":680939,"Eolink":681014,"APISpace":681015,"Apinto":681016,"json":681017,"webhook":681018,"linux":681019,"xinzengfenzu":681787,"circle-right-up":694557,"circle-right-down":694558,"APIjiekou-7mme3dcg":707431,"connection-box":707736,"system":707739,"form-one":707741,"yingyong-7mmhj11e":707742,"jiankongshexiangtou":707743,"file-cabinet":707744,"network-tree":707749,"search":708142,"find":708144,"circle-right-up-7mnlo5g9":708145,"circle-right-down-7mnlphn2":708146,"reduce-one":708147,"tool":708181,"shangxianguanli-new":709715,"hamburger-button":808898,"puzzle":808900,"keyline":808916,"daohang":810363,"lanjieqiguanli":810396,"shop":818250,"xiangmu":818340};for(var _i in nm){window.__iconpark__[_i] = obj[nm[_i]]}})();"object"!=typeof globalThis&&(Object.prototype.__defineGetter__("__magic__",function(){return this}),__magic__.globalThis=__magic__,delete Object.prototype.__magic__);(()=>{"use strict";var t={816:(t,e,i)=>{var s,r,o,n;i.d(e,{Vm:()=>z,dy:()=>P,Jb:()=>x,Ld:()=>$,sY:()=>T,YP:()=>A});const l=globalThis.trustedTypes,a=l?l.createPolicy("lit-html",{createHTML:t=>t}):void 0,h=`lit$${(Math.random()+"").slice(9)}$`,c="?"+h,d=`<${c}>`,u=document,p=(t="")=>u.createComment(t),v=t=>null===t||"object"!=typeof t&&"function"!=typeof t,f=Array.isArray,y=t=>{var e;return f(t)||"function"==typeof(null===(e=t)||void 0===e?void 0:e[Symbol.iterator])},m=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,g=/-->/g,b=/>/g,S=/>|[ \n \r](?:([^\s"'>=/]+)([ \n \r]*=[ \n \r]*(?:[^ \n \r"'`<>=]|("|')|))|$)/g,w=/'/g,k=/"/g,E=/^(?:script|style|textarea)$/i,C=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),P=C(1),A=C(2),x=Symbol.for("lit-noChange"),$=Symbol.for("lit-nothing"),O=new WeakMap,T=(t,e,i)=>{var s,r;const o=null!==(s=null==i?void 0:i.renderBefore)&&void 0!==s?s:e;let n=o._$litPart$;if(void 0===n){const t=null!==(r=null==i?void 0:i.renderBefore)&&void 0!==r?r:null;o._$litPart$=n=new H(e.insertBefore(p(),t),t,void 0,i)}return n.I(t),n},R=u.createTreeWalker(u,129,null,!1),_=(t,e)=>{const i=t.length-1,s=[];let r,o=2===e?"":"",n=m;for(let e=0;e"===a[0]?(n=null!=r?r:m,c=-1):void 0===a[1]?c=-2:(c=n.lastIndex-a[2].length,l=a[1],n=void 0===a[3]?S:'"'===a[3]?k:w):n===k||n===w?n=S:n===g||n===b?n=m:(n=S,r=void 0);const p=n===S&&t[e+1].startsWith("/>")?" ":"";o+=n===m?i+d:c>=0?(s.push(l),i.slice(0,c)+"$lit$"+i.slice(c)+h+p):i+h+(-2===c?(s.push(void 0),e):p)}const l=o+(t[i]||"")+(2===e?"":"");return[void 0!==a?a.createHTML(l):l,s]};class N{constructor({strings:t,_$litType$:e},i){let s;this.parts=[];let r=0,o=0;const n=t.length-1,a=this.parts,[d,u]=_(t,e);if(this.el=N.createElement(d,i),R.currentNode=this.el.content,2===e){const t=this.el.content,e=t.firstChild;e.remove(),t.append(...e.childNodes)}for(;null!==(s=R.nextNode())&&a.length0){s.textContent=l?l.emptyScript:"";for(let i=0;i2||""!==i[0]||""!==i[1]?(this.H=Array(i.length-1).fill($),this.strings=i):this.H=$}get tagName(){return this.element.tagName}I(t,e=this,i,s){const r=this.strings;let o=!1;if(void 0===r)t=U(this,t,e,0),o=!v(t)||t!==this.H&&t!==x,o&&(this.H=t);else{const s=t;let n,l;for(t=r[0],n=0;n{i.r(e),i.d(e,{customElement:()=>s,eventOptions:()=>a,property:()=>o,query:()=>h,queryAll:()=>c,queryAssignedNodes:()=>v,queryAsync:()=>d,state:()=>n});const s=t=>e=>"function"==typeof e?((t,e)=>(window.customElements.define(t,e),e))(t,e):((t,e)=>{const{kind:i,elements:s}=e;return{kind:i,elements:s,finisher(e){window.customElements.define(t,e)}}})(t,e),r=(t,e)=>"method"===e.kind&&e.descriptor&&!("value"in e.descriptor)?{...e,finisher(i){i.createProperty(e.key,t)}}:{kind:"field",key:Symbol(),placement:"own",descriptor:{},originalKey:e.key,initializer(){"function"==typeof e.initializer&&(this[e.key]=e.initializer.call(this))},finisher(i){i.createProperty(e.key,t)}};function o(t){return(e,i)=>void 0!==i?((t,e,i)=>{e.constructor.createProperty(i,t)})(t,e,i):r(t,e)}function n(t){return o({...t,state:!0,attribute:!1})}const l=({finisher:t,descriptor:e})=>(i,s)=>{var r;if(void 0===s){const s=null!==(r=i.originalKey)&&void 0!==r?r:i.key,o=null!=e?{kind:"method",placement:"prototype",key:s,descriptor:e(i.key)}:{...i,key:s};return null!=t&&(o.finisher=function(e){t(e,s)}),o}{const r=i.constructor;void 0!==e&&Object.defineProperty(i,s,e(s)),null==t||t(r,s)}};function a(t){return l({finisher:(e,i)=>{Object.assign(e.prototype[i],t)}})}function h(t,e){return l({descriptor:i=>{const s={get(){var e;return null===(e=this.renderRoot)||void 0===e?void 0:e.querySelector(t)},enumerable:!0,configurable:!0};if(e){const e="symbol"==typeof i?Symbol():"__"+i;s.get=function(){var i;return void 0===this[e]&&(this[e]=null===(i=this.renderRoot)||void 0===i?void 0:i.querySelector(t)),this[e]}}return s}})}function c(t){return l({descriptor:e=>({get(){var e;return null===(e=this.renderRoot)||void 0===e?void 0:e.querySelectorAll(t)},enumerable:!0,configurable:!0})})}function d(t){return l({descriptor:e=>({async get(){var e;return await this.updateComplete,null===(e=this.renderRoot)||void 0===e?void 0:e.querySelector(t)},enumerable:!0,configurable:!0})})}const u=Element.prototype,p=u.msMatchesSelector||u.webkitMatchesSelector;function v(t="",e=!1,i=""){return l({descriptor:s=>({get(){var s,r;const o="slot"+(t?`[name=${t}]`:":not([name])");let n=null===(r=null===(s=this.renderRoot)||void 0===s?void 0:s.querySelector(o))||void 0===r?void 0:r.assignedNodes({flatten:e});return n&&i&&(n=n.filter((t=>t.nodeType===Node.ELEMENT_NODE&&(t.matches?t.matches(i):p.call(t,i))))),n},enumerable:!0,configurable:!0})})}},23:(t,e,i)=>{i.r(e),i.d(e,{unsafeSVG:()=>l});const s=t=>(...e)=>({_$litDirective$:t,values:e});var r=i(816);class o extends class{constructor(t){}T(t,e,i){this.Σdt=t,this.M=e,this.Σct=i}S(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}}{constructor(t){if(super(t),this.vt=r.Ld,2!==t.type)throw Error(this.constructor.directiveName+"() can only be used in child bindings")}render(t){if(t===r.Ld)return this.Vt=void 0,this.vt=t;if(t===r.Jb)return t;if("string"!=typeof t)throw Error(this.constructor.directiveName+"() called with a non-string value");if(t===this.vt)return this.Vt;this.vt=t;const e=[t];return e.raw=e,this.Vt={_$litType$:this.constructor.resultType,strings:e,values:[]}}}o.directiveName="unsafeHTML",o.resultType=1,s(o);class n extends o{}n.directiveName="unsafeSVG",n.resultType=2;const l=s(n)},249:(t,e,i)=>{i.r(e),i.d(e,{CSSResult:()=>n,LitElement:()=>x,ReactiveElement:()=>b,UpdatingElement:()=>A,_Σ:()=>s.Vm,_Φ:()=>$,adoptStyles:()=>c,css:()=>h,defaultConverter:()=>y,getCompatibleStyle:()=>d,html:()=>s.dy,noChange:()=>s.Jb,notEqual:()=>m,nothing:()=>s.Ld,render:()=>s.sY,supportsAdoptingStyleSheets:()=>r,svg:()=>s.YP,unsafeCSS:()=>l});var s=i(816);const r=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,o=Symbol();class n{constructor(t,e){if(e!==o)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t}get styleSheet(){return r&&void 0===this.t&&(this.t=new CSSStyleSheet,this.t.replaceSync(this.cssText)),this.t}toString(){return this.cssText}}const l=t=>new n(t+"",o),a=new Map,h=(t,...e)=>{const i=e.reduce(((e,i,s)=>e+(t=>{if(t instanceof n)return t.cssText;if("number"==typeof t)return t;throw Error(`Value passed to 'css' function must be a 'css' function result: ${t}. Use 'unsafeCSS' to pass non-literal values, but\n take care to ensure page security.`)})(i)+t[s+1]),t[0]);let s=a.get(i);return void 0===s&&a.set(i,s=new n(i,o)),s},c=(t,e)=>{r?t.adoptedStyleSheets=e.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):e.forEach((e=>{const i=document.createElement("style");i.textContent=e.cssText,t.appendChild(i)}))},d=r?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return l(e)})(t):t;var u,p,v,f;const y={toAttribute(t,e){switch(e){case Boolean:t=t?"":null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},m=(t,e)=>e!==t&&(e==e||t==t),g={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:m};class b extends HTMLElement{constructor(){super(),this.Πi=new Map,this.Πo=void 0,this.Πl=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this.Πh=null,this.u()}static addInitializer(t){var e;null!==(e=this.v)&&void 0!==e||(this.v=[]),this.v.push(t)}static get observedAttributes(){this.finalize();const t=[];return this.elementProperties.forEach(((e,i)=>{const s=this.Πp(i,e);void 0!==s&&(this.Πm.set(s,i),t.push(s))})),t}static createProperty(t,e=g){if(e.state&&(e.attribute=!1),this.finalize(),this.elementProperties.set(t,e),!e.noAccessor&&!this.prototype.hasOwnProperty(t)){const i="symbol"==typeof t?Symbol():"__"+t,s=this.getPropertyDescriptor(t,i,e);void 0!==s&&Object.defineProperty(this.prototype,t,s)}}static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(s){const r=this[t];this[e]=s,this.requestUpdate(t,r,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||g}static finalize(){if(this.hasOwnProperty("finalized"))return!1;this.finalized=!0;const t=Object.getPrototypeOf(this);if(t.finalize(),this.elementProperties=new Map(t.elementProperties),this.Πm=new Map,this.hasOwnProperty("properties")){const t=this.properties,e=[...Object.getOwnPropertyNames(t),...Object.getOwnPropertySymbols(t)];for(const i of e)this.createProperty(i,t[i])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(d(t))}else void 0!==t&&e.push(d(t));return e}static Πp(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}u(){var t;this.Πg=new Promise((t=>this.enableUpdating=t)),this.L=new Map,this.Π_(),this.requestUpdate(),null===(t=this.constructor.v)||void 0===t||t.forEach((t=>t(this)))}addController(t){var e,i;(null!==(e=this.ΠU)&&void 0!==e?e:this.ΠU=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(i=t.hostConnected)||void 0===i||i.call(t))}removeController(t){var e;null===(e=this.ΠU)||void 0===e||e.splice(this.ΠU.indexOf(t)>>>0,1)}Π_(){this.constructor.elementProperties.forEach(((t,e)=>{this.hasOwnProperty(e)&&(this.Πi.set(e,this[e]),delete this[e])}))}createRenderRoot(){var t;const e=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return c(e,this.constructor.elementStyles),e}connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this.ΠU)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostConnected)||void 0===e?void 0:e.call(t)})),this.Πl&&(this.Πl(),this.Πo=this.Πl=void 0)}enableUpdating(t){}disconnectedCallback(){var t;null===(t=this.ΠU)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostDisconnected)||void 0===e?void 0:e.call(t)})),this.Πo=new Promise((t=>this.Πl=t))}attributeChangedCallback(t,e,i){this.K(t,i)}Πj(t,e,i=g){var s,r;const o=this.constructor.Πp(t,i);if(void 0!==o&&!0===i.reflect){const n=(null!==(r=null===(s=i.converter)||void 0===s?void 0:s.toAttribute)&&void 0!==r?r:y.toAttribute)(e,i.type);this.Πh=t,null==n?this.removeAttribute(o):this.setAttribute(o,n),this.Πh=null}}K(t,e){var i,s,r;const o=this.constructor,n=o.Πm.get(t);if(void 0!==n&&this.Πh!==n){const t=o.getPropertyOptions(n),l=t.converter,a=null!==(r=null!==(s=null===(i=l)||void 0===i?void 0:i.fromAttribute)&&void 0!==s?s:"function"==typeof l?l:null)&&void 0!==r?r:y.fromAttribute;this.Πh=n,this[n]=a(e,t.type),this.Πh=null}}requestUpdate(t,e,i){let s=!0;void 0!==t&&(((i=i||this.constructor.getPropertyOptions(t)).hasChanged||m)(this[t],e)?(this.L.has(t)||this.L.set(t,e),!0===i.reflect&&this.Πh!==t&&(void 0===this.Πk&&(this.Πk=new Map),this.Πk.set(t,i))):s=!1),!this.isUpdatePending&&s&&(this.Πg=this.Πq())}async Πq(){this.isUpdatePending=!0;try{for(await this.Πg;this.Πo;)await this.Πo}catch(t){Promise.reject(t)}const t=this.performUpdate();return null!=t&&await t,!this.isUpdatePending}performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,this.Πi&&(this.Πi.forEach(((t,e)=>this[e]=t)),this.Πi=void 0);let e=!1;const i=this.L;try{e=this.shouldUpdate(i),e?(this.willUpdate(i),null===(t=this.ΠU)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostUpdate)||void 0===e?void 0:e.call(t)})),this.update(i)):this.Π$()}catch(t){throw e=!1,this.Π$(),t}e&&this.E(i)}willUpdate(t){}E(t){var e;null===(e=this.ΠU)||void 0===e||e.forEach((t=>{var e;return null===(e=t.hostUpdated)||void 0===e?void 0:e.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}Π$(){this.L=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this.Πg}shouldUpdate(t){return!0}update(t){void 0!==this.Πk&&(this.Πk.forEach(((t,e)=>this.Πj(e,this[e],t))),this.Πk=void 0),this.Π$()}updated(t){}firstUpdated(t){}}var S,w,k,E,C,P;b.finalized=!0,b.shadowRootOptions={mode:"open"},null===(p=(u=globalThis).reactiveElementPlatformSupport)||void 0===p||p.call(u,{ReactiveElement:b}),(null!==(v=(f=globalThis).reactiveElementVersions)&&void 0!==v?v:f.reactiveElementVersions=[]).push("1.0.0-rc.1");const A=b;(null!==(S=(P=globalThis).litElementVersions)&&void 0!==S?S:P.litElementVersions=[]).push("3.0.0-rc.1");class x extends b{constructor(){super(...arguments),this.renderOptions={host:this},this.Φt=void 0}createRenderRoot(){var t,e;const i=super.createRenderRoot();return null!==(t=(e=this.renderOptions).renderBefore)&&void 0!==t||(e.renderBefore=i.firstChild),i}update(t){const e=this.render();super.update(t),this.Φt=(0,s.sY)(e,this.renderRoot,this.renderOptions)}connectedCallback(){var t;super.connectedCallback(),null===(t=this.Φt)||void 0===t||t.setConnected(!0)}disconnectedCallback(){var t;super.disconnectedCallback(),null===(t=this.Φt)||void 0===t||t.setConnected(!1)}render(){return s.Jb}}x.finalized=!0,x._$litElement$=!0,null===(k=(w=globalThis).litElementHydrateSupport)||void 0===k||k.call(w,{LitElement:x}),null===(C=(E=globalThis).litElementPlatformSupport)||void 0===C||C.call(E,{LitElement:x});const $={K:(t,e,i)=>{t.K(e,i)},L:t=>t.L}},409:function(t,e,i){var s=this&&this.__decorate||function(t,e,i,s){var r,o=arguments.length,n=o<3?e:null===s?s=Object.getOwnPropertyDescriptor(e,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,i,s);else for(var l=t.length-1;l>=0;l--)(r=t[l])&&(n=(o<3?r(n):o>3?r(e,i,n):r(e,i))||n);return o>3&&n&&Object.defineProperty(e,i,n),n};Object.defineProperty(e,"__esModule",{value:!0}),e.IconparkIconElement=void 0;const r=i(249),o=i(26),n=i(23),l={color:1,fill:1,stroke:1},a={STROKE:{trackAttr:"data-follow-stroke",rawAttr:"stroke"},FILL:{trackAttr:"data-follow-fill",rawAttr:"fill"}};class h extends r.LitElement{constructor(){super(...arguments),this.name="",this.identifyer="",this.size="1em"}get _width(){return this.width||this.size}get _height(){return this.height||this.size}get _stroke(){return this.stroke||this.color}get _fill(){return this.fill||this.color}get SVGConfig(){return(window.__iconpark__||{})[this.identifyer]||(window.__iconpark__||{})[this.name]||{viewBox:"0 0 0 0",content:""}}connectedCallback(){super.connectedCallback(),setTimeout((()=>{this.monkeyPatch("STROKE",!0),this.monkeyPatch("FILL",!0)}))}monkeyPatch(t,e){switch(t){case"STROKE":this.updateDOMByHand(this.strokeAppliedNodes,"STROKE",this._stroke,!!e);break;case"FILL":this.updateDOMByHand(this.fillAppliedNodes,"FILL",this._fill,!!e)}}updateDOMByHand(t,e,i,s){!i&&s||t&&t.forEach((t=>{i&&i===t.getAttribute(a[e].rawAttr)||t.setAttribute(a[e].rawAttr,i||t.getAttribute(a[e].trackAttr))}))}attributeChangedCallback(t,e,i){super.attributeChangedCallback(t,e,i),"name"===t||"identifyer"===t?setTimeout((()=>{this.monkeyPatch("STROKE"),this.monkeyPatch("FILL")})):l[t]&&(this.monkeyPatch("STROKE"),this.monkeyPatch("FILL"))}render(){return r.svg`${n.unsafeSVG(this.SVGConfig.content)}`}}h.styles=r.css`:host {display: inline-flex; align-items: center; justify-content: center;} :host([spin]) svg {animation: iconpark-spin 1s infinite linear;} :host([spin][rtl]) svg {animation: iconpark-spin-rtl 1s infinite linear;} :host([rtl]) svg {transform: scaleX(-1);} @keyframes iconpark-spin {0% { -webkit-transform: rotate(0); transform: rotate(0);} 100% {-webkit-transform: rotate(360deg); transform: rotate(360deg);}} @keyframes iconpark-spin-rtl {0% {-webkit-transform: scaleX(-1) rotate(0); transform: scaleX(-1) rotate(0);} 100% {-webkit-transform: scaleX(-1) rotate(360deg); transform: scaleX(-1) rotate(360deg);}}`,s([o.property({reflect:!0})],h.prototype,"name",void 0),s([o.property({reflect:!0,attribute:"icon-id"})],h.prototype,"identifyer",void 0),s([o.property({reflect:!0})],h.prototype,"color",void 0),s([o.property({reflect:!0})],h.prototype,"stroke",void 0),s([o.property({reflect:!0})],h.prototype,"fill",void 0),s([o.property({reflect:!0})],h.prototype,"size",void 0),s([o.property({reflect:!0})],h.prototype,"width",void 0),s([o.property({reflect:!0})],h.prototype,"height",void 0),s([o.queryAll(`[${a.STROKE.trackAttr}]`)],h.prototype,"strokeAppliedNodes",void 0),s([o.queryAll(`[${a.FILL.trackAttr}]`)],h.prototype,"fillAppliedNodes",void 0),e.IconparkIconElement=h,customElements.get("iconpark-icon")||customElements.define("iconpark-icon",h)}},e={};function i(s){var r=e[s];if(void 0!==r)return r.exports;var o=e[s]={exports:{}};return t[s].call(o.exports,o,o.exports,i),o.exports}i.d=(t,e)=>{for(var s in e)i.o(e,s)&&!i.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:e[s]})},i.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),i.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},i(409)})(); + \ No newline at end of file diff --git a/frontend/packages/businessEntry/public/iconpark_eolink.js b/frontend/packages/businessEntry/public/iconpark_eolink.js new file mode 100644 index 00000000..5c7f6392 --- /dev/null +++ b/frontend/packages/businessEntry/public/iconpark_eolink.js @@ -0,0 +1,7 @@ +/* + * @Date: 2024-05-06 09:53:45 + * @LastEditors: maggieyyy + * @LastEditTime: 2024-05-06 09:53:50 + * @FilePath: \frontend\packages\core\src\assets\iconpark_eolink.js + */ +(function(){window.__iconpark__=window.__iconpark__||{};var obj=JSON.parse("{\"647367\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"684408\":{\"viewBox\":\"0 0 194 194\",\"content\":\"\"},\"684409\":{\"viewBox\":\"0 0 194 194\",\"content\":\"\"},\"684411\":{\"viewBox\":\"0 0 119.19 102.5\",\"content\":\"\"},\"684412\":{\"viewBox\":\"0 0 108.55 93.99\",\"fill\":\"currentColor\",\"content\":\"\"},\"684413\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"684414\":{\"viewBox\":\"0 0 1024 1024\",\"fill\":\"currentColor\",\"content\":\"\"},\"686740\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686741\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686742\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686743\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686744\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686745\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686746\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686747\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686748\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686749\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686750\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686751\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686752\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686753\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686754\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686993\":{\"viewBox\":\"0 0 38.22 22.18\",\"fill\":\"currentColor\",\"content\":\"\"},\"687741\":{\"viewBox\":\"0 0 194 194\",\"content\":\"\"},\"687742\":{\"viewBox\":\"0 0 194 194\",\"content\":\"\"},\"691262\":{\"viewBox\":\"0 0 194 194\",\"content\":\"\"},\"691537\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"691538\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"691806\":{\"viewBox\":\"0 0 194 194\",\"content\":\"\"},\"695738\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695739\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695740\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695741\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695742\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695743\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695746\":{\"viewBox\":\"0 0 1185 1024\",\"fill\":\"currentColor\",\"content\":\"\"},\"695747\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695748\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695750\":{\"viewBox\":\"0 0 1024 1024\",\"fill\":\"currentColor\",\"content\":\"\"},\"695751\":{\"viewBox\":\"0 0 1024 1024\",\"fill\":\"currentColor\",\"content\":\"\"},\"695752\":{\"viewBox\":\"0 0 1024 1024\",\"fill\":\"currentColor\",\"content\":\"\"},\"695754\":{\"viewBox\":\"0 0 1024 1024\",\"fill\":\"currentColor\",\"content\":\"\"},\"695755\":{\"viewBox\":\"0 0 1024 1024\",\"fill\":\"currentColor\",\"content\":\"\"},\"695756\":{\"viewBox\":\"0 0 1024 1024\",\"fill\":\"currentColor\",\"content\":\"\"},\"695758\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695759\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695760\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695761\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695762\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695763\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695764\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695801\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695802\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695803\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695804\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695805\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695806\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695807\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695810\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695811\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695812\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695817\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695818\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695819\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695820\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695821\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695822\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695828\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695829\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695830\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695831\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695833\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695834\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695835\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695836\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695837\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695838\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695839\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695840\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695841\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695842\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695844\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695845\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695846\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695865\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695867\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695868\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695869\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695870\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695876\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695877\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695878\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695883\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695884\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695886\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695887\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695888\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695889\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695890\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695891\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695892\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695893\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695896\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695899\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695900\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695901\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695902\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695903\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695904\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695905\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695906\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695907\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695908\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695909\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695913\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695914\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695915\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695916\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695933\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695934\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695935\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695936\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695938\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695940\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695941\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695942\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695944\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695945\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695946\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695947\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695948\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695950\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695951\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695953\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695954\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695955\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695956\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695957\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695958\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695959\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695960\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695961\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695962\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695963\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695964\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695966\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695967\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695968\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695969\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695971\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695972\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695973\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695975\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695978\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695979\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695980\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695981\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695982\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695984\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695985\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695986\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695987\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695988\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695990\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695993\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695995\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695997\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695999\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696002\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696003\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696004\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696005\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696007\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696009\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696010\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696011\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696012\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696013\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696014\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696015\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696016\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696017\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696018\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696019\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696020\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696021\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696022\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696023\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696024\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696025\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696027\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696028\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696029\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696030\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696031\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696032\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696033\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696034\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696035\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696036\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696037\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696038\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696039\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696040\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696041\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696042\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696043\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696044\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696045\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696046\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696048\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696049\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696660\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696661\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"744163\":{\"viewBox\":\"0 0 1024 1024\",\"fill\":\"currentColor\",\"content\":\"\"},\"744173\":{\"viewBox\":\"0 0 128 128\",\"fill\":\"none\",\"content\":\"\"},\"744175\":{\"viewBox\":\"0 0 128 128\",\"fill\":\"none\",\"content\":\"\"},\"750656\":{\"viewBox\":\"0 0 61 61\",\"fill\":\"none\",\"content\":\"\"},\"752737\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"756392\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"757321\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"757499\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"757504\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"757518\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"757519\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"757520\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"757521\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"757616\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"757650\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"767277\":{\"viewBox\":\"0 0 20 20\",\"fill\":\"none\",\"content\":\"\"},\"767278\":{\"viewBox\":\"0 0 20 20\",\"fill\":\"none\",\"content\":\"\"},\"775549\":{\"viewBox\":\"0 0 18 14\",\"fill\":\"none\",\"content\":\"\"},\"779333\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"none\",\"content\":\"\"},\"779418\":{\"viewBox\":\"0 0 1024 1024\",\"content\":\"\"},\"779705\":{\"viewBox\":\"0 0 20 20\",\"fill\":\"none\",\"content\":\"\"},\"779706\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"none\",\"content\":\"\"},\"787702\":{\"viewBox\":\"0 0 1024 1024\",\"content\":\"\"},\"788577\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"802334\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"804269\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"804612\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"804614\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"806103\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"813707\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"815901\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"820089\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"826687\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"854318\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"855246\":{\"viewBox\":\"0 0 16 16\",\"fill\":\"none\",\"content\":\"\"},\"855247\":{\"viewBox\":\"0 0 16 16\",\"fill\":\"none\",\"content\":\"\"},\"855248\":{\"viewBox\":\"0 0 16 16\",\"fill\":\"none\",\"content\":\"\"},\"855927\":{\"viewBox\":\"0 0 83 20\",\"fill\":\"none\",\"content\":\"\"},\"855928\":{\"viewBox\":\"0 0 68 24\",\"fill\":\"none\",\"content\":\"\"},\"855929\":{\"viewBox\":\"0 0 66 24\",\"fill\":\"none\",\"content\":\"\"},\"855938\":{\"viewBox\":\"0 0 198 72\",\"fill\":\"none\",\"content\":\"\"},\"857931\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"857985\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"861388\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"876705\":{\"viewBox\":\"0 0 16 16\",\"fill\":\"none\",\"content\":\"\"},\"884011\":{\"viewBox\":\"0 0 20 20\",\"fill\":\"none\",\"content\":\"\"},\"885387\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"897026\":{\"viewBox\":\"0 0 250 250\",\"fill\":\"none\",\"content\":\"\"},\"915485\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"929257\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"932197\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"949128\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"970590\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"973801\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"985435\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1002903\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"none\",\"content\":\"\"},\"1021623\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1021686\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1035721\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1035737\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1037074\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1037815\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1037816\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1037817\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1039918\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1042170\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1042171\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"}}");for(var _k in obj){window.__iconpark__[_k] = obj[_k]};var nm={"round-fill":647367,"apinto-pro-icon":684408,"apinto-icon":684409,"apinto-pro":684411,"apinto":684412,"check-circle":684413,"apispace":684414,"auto-generate-api":686740,"compare-api":686741,"multi-protocal":686742,"read-good":686743,"richdoc":686744,"mockapi":686745,"script-support":686746,"diy-test":686747,"send":686748,"stereo-perspective":686749,"automatic-robot":686750,"switch-env":686751,"flash":686752,"chart-pie":686753,"date-drive":686754,"apistudio":686993,"postcat-icon":687741,"postcat":687742,"apistudio-icon":691262,"update-rotation":691537,"page":691538,"apispace-icon":691806,"avatar":695738,"people":695739,"people-minus":695740,"people-plus":695741,"peoples":695742,"user-business":695743,"folder-close-fill":695746,"windows":695747,"github":695748,"qq":695750,"browser-chrome":695751,"linux":695752,"edge":695754,"wechat":695755,"browser":695756,"gitlab":695758,"apple":695759,"alipay":695760,"facebook":695761,"twitter":695762,"paypal":695763,"new-lark":695764,"delete":695801,"return":695802,"search":695803,"import":695804,"export":695805,"add":695806,"add-child":695807,"file-addition":695810,"add-circle":695811,"minus":695812,"close":695817,"close-small":695818,"check-small":695819,"check":695820,"code-terminal":695821,"code":695822,"preview-open":695828,"preview-close":695829,"folder-close":695830,"folder-open":695831,"upload":695833,"download":695834,"copy":695835,"upload-file":695836,"compare":695837,"edit":695838,"share":695839,"share-all":695840,"share-url-fill":695841,"share-url":695842,"back":695844,"back-fill":695845,"share-fill":695846,"sort":695865,"filter":695867,"reduce":695868,"done-all":695869,"full-selection":695870,"right-bar":695876,"left-bar":695877,"direction-adjustment":695878,"down-small":695883,"left-small":695884,"right-small":695886,"right-one":695887,"right":695888,"up":695889,"up-one":695890,"up-small":695891,"up-two":695892,"down-two":695893,"enter":695896,"down":695899,"left":695900,"down-one":695901,"left-two":695902,"right-two":695903,"left-one":695904,"more":695905,"expand-left":695906,"expand-right":695907,"column":695908,"center-alignment":695909,"list-add":695913,"sort-amount-down":695914,"sort-amount-up":695915,"list":695916,"remind":695933,"close-remind":695934,"api":695935,"rocket":695936,"monitor":695938,"robot":695940,"plan":695941,"application":695942,"chart-proportion":695944,"data":695945,"chart-line":695946,"pie-10":695947,"pie":695948,"chart-bubble":695950,"cube":695951,"application-menu":695953,"crown":695954,"crown-fill":695955,"market":695956,"file-word":695957,"file-excel":695958,"hashtag-key":695959,"file-hash":695960,"refresh":695961,"order":695962,"command":695963,"branch":695964,"page-template":695966,"smart-optimization":695967,"assembly-line":695968,"stopwatch":695969,"checklist":695971,"menu-fold":695972,"menu-unfold":695973,"alarm":695975,"protection":695978,"caution":695979,"openapi":695980,"webhook":695981,"holding-hands":695982,"support":695984,"agreement":695985,"community":695986,"roadmap":695987,"family-7knl2ae1":695988,"smiling-face":695990,"play-fill":695993,"play":695995,"pause":695997,"magic":695999,"whole-site-accelerator":696002,"link-cloud-faild":696003,"link-cloud-sucess":696004,"translate":696005,"funds":696007,"unhappy-face":696009,"message":696010,"connection-arrow":696011,"loading":696012,"fork":696013,"quote":696014,"headset":696015,"attention":696016,"theme":696017,"keyboard":696018,"briefcase":696019,"star":696020,"star-7knmka28":696021,"protect":696022,"finance":696023,"setting":696024,"link":696025,"undo":696027,"inbox-success":696028,"home":696029,"local":696030,"laptop":696031,"view-list":696032,"lock":696033,"unlock":696034,"lightning":696035,"file-text":696036,"cooperative-handshake":696037,"navigation":696038,"view-grid-detail":696039,"help":696040,"history":696041,"logout-7knnioon":696042,"chinese":696043,"calendar":696044,"play-cycle":696045,"world":696046,"plugins":696048,"link-cloud":696049,"book":696660,"table-report":696661,"qiyeweixin":744163,"Oauth":744173,"dingding":744175,"eolink":750656,"tool":752737,"category-management":756392,"folder-code-one":757321,"link-three-8ah7lifn":757499,"download-two-8ah85008":757504,"quanjusuoxiao1":757518,"quanjufangda21":757519,"quanjusuoxiao211":757520,"quanjufangda1":757521,"wenjianshezhi":757616,"key":757650,"zidingyijiaoben":767277,"tiqubianliang":767278,"mock":775549,"tongzhishezhi":779333,"csdn":779418,"ceshibaogao":779705,"biangengtongzhi":779706,"icon-api":787702,"youjian":788577,"pushpin":802334,"announcement":804269,"collapse-text-input":804612,"zhankai":804614,"replay-music":806103,"download-web":813707,"permissions":815901,"file-editing":820089,"wallet":826687,"file-focus":854318,"pingpu-9a913n0n":855246,"zuoyoufenping-9a913n1f":855247,"shangxiafenping-9a913n1i":855248,"Paypal11":855927,"zhifubaozhifu1":855928,"weixinzhifu11":855929,"weixinzhifu":855938,"update-rotation-9and40f5":857931,"terminal":857985,"switch":861388,"zhinengrucan":876705,"biaoqian-banbenleixinzeng":884011,"book-open":885387,"morentouxiang-2":897026,"xiajia":915485,"drag":929257,"new-up":932197,"rss":949128,"yewuchangjing":970590,"newlybuild":973801,"bianji":985435,"jiekoushouquan":1002903,"interfacefenzutubiao":1021623,"yidong":1021686,"link-one":1035721,"canshugouzaoqi":1035737,"bianliang":1037074,"tars":1037815,"if":1037816,"tars-2":1037817,"yingyongguanxi":1039918,"save-one":1042170,"save":1042171};for(var _i in nm){window.__iconpark__[_i] = obj[nm[_i]]}})();"object"!=typeof globalThis&&(Object.prototype.__defineGetter__("__magic__",function(){return this}),__magic__.globalThis=__magic__,delete Object.prototype.__magic__);(()=>{"use strict";var t={816:(t,e,i)=>{var s,r,o,n;i.d(e,{Vm:()=>z,dy:()=>P,Jb:()=>x,Ld:()=>$,sY:()=>T,YP:()=>A});const l=globalThis.trustedTypes,a=l?l.createPolicy("lit-html",{createHTML:t=>t}):void 0,h=`lit$${(Math.random()+"").slice(9)}$`,c="?"+h,d=`<${c}>`,u=document,p=(t="")=>u.createComment(t),v=t=>null===t||"object"!=typeof t&&"function"!=typeof t,f=Array.isArray,y=t=>{var e;return f(t)||"function"==typeof(null===(e=t)||void 0===e?void 0:e[Symbol.iterator])},m=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,g=/-->/g,b=/>/g,S=/>|[ \n \r](?:([^\s"'>=/]+)([ \n \r]*=[ \n \r]*(?:[^ \n \r"'`<>=]|("|')|))|$)/g,w=/'/g,k=/"/g,E=/^(?:script|style|textarea)$/i,C=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),P=C(1),A=C(2),x=Symbol.for("lit-noChange"),$=Symbol.for("lit-nothing"),O=new WeakMap,T=(t,e,i)=>{var s,r;const o=null!==(s=null==i?void 0:i.renderBefore)&&void 0!==s?s:e;let n=o._$litPart$;if(void 0===n){const t=null!==(r=null==i?void 0:i.renderBefore)&&void 0!==r?r:null;o._$litPart$=n=new H(e.insertBefore(p(),t),t,void 0,i)}return n.I(t),n},R=u.createTreeWalker(u,129,null,!1),_=(t,e)=>{const i=t.length-1,s=[];let r,o=2===e?"":"",n=m;for(let e=0;e"===a[0]?(n=null!=r?r:m,c=-1):void 0===a[1]?c=-2:(c=n.lastIndex-a[2].length,l=a[1],n=void 0===a[3]?S:'"'===a[3]?k:w):n===k||n===w?n=S:n===g||n===b?n=m:(n=S,r=void 0);const p=n===S&&t[e+1].startsWith("/>")?" ":"";o+=n===m?i+d:c>=0?(s.push(l),i.slice(0,c)+"$lit$"+i.slice(c)+h+p):i+h+(-2===c?(s.push(void 0),e):p)}const l=o+(t[i]||"")+(2===e?"":"");return[void 0!==a?a.createHTML(l):l,s]};class N{constructor({strings:t,_$litType$:e},i){let s;this.parts=[];let r=0,o=0;const n=t.length-1,a=this.parts,[d,u]=_(t,e);if(this.el=N.createElement(d,i),R.currentNode=this.el.content,2===e){const t=this.el.content,e=t.firstChild;e.remove(),t.append(...e.childNodes)}for(;null!==(s=R.nextNode())&&a.length0){s.textContent=l?l.emptyScript:"";for(let i=0;i2||""!==i[0]||""!==i[1]?(this.H=Array(i.length-1).fill($),this.strings=i):this.H=$}get tagName(){return this.element.tagName}I(t,e=this,i,s){const r=this.strings;let o=!1;if(void 0===r)t=U(this,t,e,0),o=!v(t)||t!==this.H&&t!==x,o&&(this.H=t);else{const s=t;let n,l;for(t=r[0],n=0;n{i.r(e),i.d(e,{customElement:()=>s,eventOptions:()=>a,property:()=>o,query:()=>h,queryAll:()=>c,queryAssignedNodes:()=>v,queryAsync:()=>d,state:()=>n});const s=t=>e=>"function"==typeof e?((t,e)=>(window.customElements.define(t,e),e))(t,e):((t,e)=>{const{kind:i,elements:s}=e;return{kind:i,elements:s,finisher(e){window.customElements.define(t,e)}}})(t,e),r=(t,e)=>"method"===e.kind&&e.descriptor&&!("value"in e.descriptor)?{...e,finisher(i){i.createProperty(e.key,t)}}:{kind:"field",key:Symbol(),placement:"own",descriptor:{},originalKey:e.key,initializer(){"function"==typeof e.initializer&&(this[e.key]=e.initializer.call(this))},finisher(i){i.createProperty(e.key,t)}};function o(t){return(e,i)=>void 0!==i?((t,e,i)=>{e.constructor.createProperty(i,t)})(t,e,i):r(t,e)}function n(t){return o({...t,state:!0,attribute:!1})}const l=({finisher:t,descriptor:e})=>(i,s)=>{var r;if(void 0===s){const s=null!==(r=i.originalKey)&&void 0!==r?r:i.key,o=null!=e?{kind:"method",placement:"prototype",key:s,descriptor:e(i.key)}:{...i,key:s};return null!=t&&(o.finisher=function(e){t(e,s)}),o}{const r=i.constructor;void 0!==e&&Object.defineProperty(i,s,e(s)),null==t||t(r,s)}};function a(t){return l({finisher:(e,i)=>{Object.assign(e.prototype[i],t)}})}function h(t,e){return l({descriptor:i=>{const s={get(){var e;return null===(e=this.renderRoot)||void 0===e?void 0:e.querySelector(t)},enumerable:!0,configurable:!0};if(e){const e="symbol"==typeof i?Symbol():"__"+i;s.get=function(){var i;return void 0===this[e]&&(this[e]=null===(i=this.renderRoot)||void 0===i?void 0:i.querySelector(t)),this[e]}}return s}})}function c(t){return l({descriptor:e=>({get(){var e;return null===(e=this.renderRoot)||void 0===e?void 0:e.querySelectorAll(t)},enumerable:!0,configurable:!0})})}function d(t){return l({descriptor:e=>({async get(){var e;return await this.updateComplete,null===(e=this.renderRoot)||void 0===e?void 0:e.querySelector(t)},enumerable:!0,configurable:!0})})}const u=Element.prototype,p=u.msMatchesSelector||u.webkitMatchesSelector;function v(t="",e=!1,i=""){return l({descriptor:s=>({get(){var s,r;const o="slot"+(t?`[name=${t}]`:":not([name])");let n=null===(r=null===(s=this.renderRoot)||void 0===s?void 0:s.querySelector(o))||void 0===r?void 0:r.assignedNodes({flatten:e});return n&&i&&(n=n.filter((t=>t.nodeType===Node.ELEMENT_NODE&&(t.matches?t.matches(i):p.call(t,i))))),n},enumerable:!0,configurable:!0})})}},23:(t,e,i)=>{i.r(e),i.d(e,{unsafeSVG:()=>l});const s=t=>(...e)=>({_$litDirective$:t,values:e});var r=i(816);class o extends class{constructor(t){}T(t,e,i){this.Σdt=t,this.M=e,this.Σct=i}S(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}}{constructor(t){if(super(t),this.vt=r.Ld,2!==t.type)throw Error(this.constructor.directiveName+"() can only be used in child bindings")}render(t){if(t===r.Ld)return this.Vt=void 0,this.vt=t;if(t===r.Jb)return t;if("string"!=typeof t)throw Error(this.constructor.directiveName+"() called with a non-string value");if(t===this.vt)return this.Vt;this.vt=t;const e=[t];return e.raw=e,this.Vt={_$litType$:this.constructor.resultType,strings:e,values:[]}}}o.directiveName="unsafeHTML",o.resultType=1,s(o);class n extends o{}n.directiveName="unsafeSVG",n.resultType=2;const l=s(n)},249:(t,e,i)=>{i.r(e),i.d(e,{CSSResult:()=>n,LitElement:()=>x,ReactiveElement:()=>b,UpdatingElement:()=>A,_Σ:()=>s.Vm,_Φ:()=>$,adoptStyles:()=>c,css:()=>h,defaultConverter:()=>y,getCompatibleStyle:()=>d,html:()=>s.dy,noChange:()=>s.Jb,notEqual:()=>m,nothing:()=>s.Ld,render:()=>s.sY,supportsAdoptingStyleSheets:()=>r,svg:()=>s.YP,unsafeCSS:()=>l});var s=i(816);const r=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,o=Symbol();class n{constructor(t,e){if(e!==o)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t}get styleSheet(){return r&&void 0===this.t&&(this.t=new CSSStyleSheet,this.t.replaceSync(this.cssText)),this.t}toString(){return this.cssText}}const l=t=>new n(t+"",o),a=new Map,h=(t,...e)=>{const i=e.reduce(((e,i,s)=>e+(t=>{if(t instanceof n)return t.cssText;if("number"==typeof t)return t;throw Error(`Value passed to 'css' function must be a 'css' function result: ${t}. Use 'unsafeCSS' to pass non-literal values, but\n take care to ensure page security.`)})(i)+t[s+1]),t[0]);let s=a.get(i);return void 0===s&&a.set(i,s=new n(i,o)),s},c=(t,e)=>{r?t.adoptedStyleSheets=e.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):e.forEach((e=>{const i=document.createElement("style");i.textContent=e.cssText,t.appendChild(i)}))},d=r?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return l(e)})(t):t;var u,p,v,f;const y={toAttribute(t,e){switch(e){case Boolean:t=t?"":null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},m=(t,e)=>e!==t&&(e==e||t==t),g={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:m};class b extends HTMLElement{constructor(){super(),this.Πi=new Map,this.Πo=void 0,this.Πl=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this.Πh=null,this.u()}static addInitializer(t){var e;null!==(e=this.v)&&void 0!==e||(this.v=[]),this.v.push(t)}static get observedAttributes(){this.finalize();const t=[];return this.elementProperties.forEach(((e,i)=>{const s=this.Πp(i,e);void 0!==s&&(this.Πm.set(s,i),t.push(s))})),t}static createProperty(t,e=g){if(e.state&&(e.attribute=!1),this.finalize(),this.elementProperties.set(t,e),!e.noAccessor&&!this.prototype.hasOwnProperty(t)){const i="symbol"==typeof t?Symbol():"__"+t,s=this.getPropertyDescriptor(t,i,e);void 0!==s&&Object.defineProperty(this.prototype,t,s)}}static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(s){const r=this[t];this[e]=s,this.requestUpdate(t,r,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||g}static finalize(){if(this.hasOwnProperty("finalized"))return!1;this.finalized=!0;const t=Object.getPrototypeOf(this);if(t.finalize(),this.elementProperties=new Map(t.elementProperties),this.Πm=new Map,this.hasOwnProperty("properties")){const t=this.properties,e=[...Object.getOwnPropertyNames(t),...Object.getOwnPropertySymbols(t)];for(const i of e)this.createProperty(i,t[i])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(d(t))}else void 0!==t&&e.push(d(t));return e}static Πp(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}u(){var t;this.Πg=new Promise((t=>this.enableUpdating=t)),this.L=new Map,this.Π_(),this.requestUpdate(),null===(t=this.constructor.v)||void 0===t||t.forEach((t=>t(this)))}addController(t){var e,i;(null!==(e=this.ΠU)&&void 0!==e?e:this.ΠU=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(i=t.hostConnected)||void 0===i||i.call(t))}removeController(t){var e;null===(e=this.ΠU)||void 0===e||e.splice(this.ΠU.indexOf(t)>>>0,1)}Π_(){this.constructor.elementProperties.forEach(((t,e)=>{this.hasOwnProperty(e)&&(this.Πi.set(e,this[e]),delete this[e])}))}createRenderRoot(){var t;const e=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return c(e,this.constructor.elementStyles),e}connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this.ΠU)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostConnected)||void 0===e?void 0:e.call(t)})),this.Πl&&(this.Πl(),this.Πo=this.Πl=void 0)}enableUpdating(t){}disconnectedCallback(){var t;null===(t=this.ΠU)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostDisconnected)||void 0===e?void 0:e.call(t)})),this.Πo=new Promise((t=>this.Πl=t))}attributeChangedCallback(t,e,i){this.K(t,i)}Πj(t,e,i=g){var s,r;const o=this.constructor.Πp(t,i);if(void 0!==o&&!0===i.reflect){const n=(null!==(r=null===(s=i.converter)||void 0===s?void 0:s.toAttribute)&&void 0!==r?r:y.toAttribute)(e,i.type);this.Πh=t,null==n?this.removeAttribute(o):this.setAttribute(o,n),this.Πh=null}}K(t,e){var i,s,r;const o=this.constructor,n=o.Πm.get(t);if(void 0!==n&&this.Πh!==n){const t=o.getPropertyOptions(n),l=t.converter,a=null!==(r=null!==(s=null===(i=l)||void 0===i?void 0:i.fromAttribute)&&void 0!==s?s:"function"==typeof l?l:null)&&void 0!==r?r:y.fromAttribute;this.Πh=n,this[n]=a(e,t.type),this.Πh=null}}requestUpdate(t,e,i){let s=!0;void 0!==t&&(((i=i||this.constructor.getPropertyOptions(t)).hasChanged||m)(this[t],e)?(this.L.has(t)||this.L.set(t,e),!0===i.reflect&&this.Πh!==t&&(void 0===this.Πk&&(this.Πk=new Map),this.Πk.set(t,i))):s=!1),!this.isUpdatePending&&s&&(this.Πg=this.Πq())}async Πq(){this.isUpdatePending=!0;try{for(await this.Πg;this.Πo;)await this.Πo}catch(t){Promise.reject(t)}const t=this.performUpdate();return null!=t&&await t,!this.isUpdatePending}performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,this.Πi&&(this.Πi.forEach(((t,e)=>this[e]=t)),this.Πi=void 0);let e=!1;const i=this.L;try{e=this.shouldUpdate(i),e?(this.willUpdate(i),null===(t=this.ΠU)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostUpdate)||void 0===e?void 0:e.call(t)})),this.update(i)):this.Π$()}catch(t){throw e=!1,this.Π$(),t}e&&this.E(i)}willUpdate(t){}E(t){var e;null===(e=this.ΠU)||void 0===e||e.forEach((t=>{var e;return null===(e=t.hostUpdated)||void 0===e?void 0:e.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}Π$(){this.L=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this.Πg}shouldUpdate(t){return!0}update(t){void 0!==this.Πk&&(this.Πk.forEach(((t,e)=>this.Πj(e,this[e],t))),this.Πk=void 0),this.Π$()}updated(t){}firstUpdated(t){}}var S,w,k,E,C,P;b.finalized=!0,b.shadowRootOptions={mode:"open"},null===(p=(u=globalThis).reactiveElementPlatformSupport)||void 0===p||p.call(u,{ReactiveElement:b}),(null!==(v=(f=globalThis).reactiveElementVersions)&&void 0!==v?v:f.reactiveElementVersions=[]).push("1.0.0-rc.1");const A=b;(null!==(S=(P=globalThis).litElementVersions)&&void 0!==S?S:P.litElementVersions=[]).push("3.0.0-rc.1");class x extends b{constructor(){super(...arguments),this.renderOptions={host:this},this.Φt=void 0}createRenderRoot(){var t,e;const i=super.createRenderRoot();return null!==(t=(e=this.renderOptions).renderBefore)&&void 0!==t||(e.renderBefore=i.firstChild),i}update(t){const e=this.render();super.update(t),this.Φt=(0,s.sY)(e,this.renderRoot,this.renderOptions)}connectedCallback(){var t;super.connectedCallback(),null===(t=this.Φt)||void 0===t||t.setConnected(!0)}disconnectedCallback(){var t;super.disconnectedCallback(),null===(t=this.Φt)||void 0===t||t.setConnected(!1)}render(){return s.Jb}}x.finalized=!0,x._$litElement$=!0,null===(k=(w=globalThis).litElementHydrateSupport)||void 0===k||k.call(w,{LitElement:x}),null===(C=(E=globalThis).litElementPlatformSupport)||void 0===C||C.call(E,{LitElement:x});const $={K:(t,e,i)=>{t.K(e,i)},L:t=>t.L}},409:function(t,e,i){var s=this&&this.__decorate||function(t,e,i,s){var r,o=arguments.length,n=o<3?e:null===s?s=Object.getOwnPropertyDescriptor(e,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,i,s);else for(var l=t.length-1;l>=0;l--)(r=t[l])&&(n=(o<3?r(n):o>3?r(e,i,n):r(e,i))||n);return o>3&&n&&Object.defineProperty(e,i,n),n};Object.defineProperty(e,"__esModule",{value:!0}),e.IconparkIconElement=void 0;const r=i(249),o=i(26),n=i(23),l={color:1,fill:1,stroke:1},a={STROKE:{trackAttr:"data-follow-stroke",rawAttr:"stroke"},FILL:{trackAttr:"data-follow-fill",rawAttr:"fill"}};class h extends r.LitElement{constructor(){super(...arguments),this.name="",this.identifyer="",this.size="1em"}get _width(){return this.width||this.size}get _height(){return this.height||this.size}get _stroke(){return this.stroke||this.color}get _fill(){return this.fill||this.color}get SVGConfig(){return(window.__iconpark__||{})[this.identifyer]||(window.__iconpark__||{})[this.name]||{viewBox:"0 0 0 0",content:""}}connectedCallback(){super.connectedCallback(),setTimeout((()=>{this.monkeyPatch("STROKE",!0),this.monkeyPatch("FILL",!0)}))}monkeyPatch(t,e){switch(t){case"STROKE":this.updateDOMByHand(this.strokeAppliedNodes,"STROKE",this._stroke,!!e);break;case"FILL":this.updateDOMByHand(this.fillAppliedNodes,"FILL",this._fill,!!e)}}updateDOMByHand(t,e,i,s){!i&&s||t&&t.forEach((t=>{i&&i===t.getAttribute(a[e].rawAttr)||t.setAttribute(a[e].rawAttr,i||t.getAttribute(a[e].trackAttr))}))}attributeChangedCallback(t,e,i){super.attributeChangedCallback(t,e,i),"name"===t||"identifyer"===t?setTimeout((()=>{this.monkeyPatch("STROKE"),this.monkeyPatch("FILL")})):l[t]&&(this.monkeyPatch("STROKE"),this.monkeyPatch("FILL"))}render(){return r.svg`${n.unsafeSVG(this.SVGConfig.content)}`}}h.styles=r.css`:host {display: inline-flex; align-items: center; justify-content: center;} :host([spin]) svg {animation: iconpark-spin 1s infinite linear;} :host([spin][rtl]) svg {animation: iconpark-spin-rtl 1s infinite linear;} :host([rtl]) svg {transform: scaleX(-1);} @keyframes iconpark-spin {0% { -webkit-transform: rotate(0); transform: rotate(0);} 100% {-webkit-transform: rotate(360deg); transform: rotate(360deg);}} @keyframes iconpark-spin-rtl {0% {-webkit-transform: scaleX(-1) rotate(0); transform: scaleX(-1) rotate(0);} 100% {-webkit-transform: scaleX(-1) rotate(360deg); transform: scaleX(-1) rotate(360deg);}}`,s([o.property({reflect:!0})],h.prototype,"name",void 0),s([o.property({reflect:!0,attribute:"icon-id"})],h.prototype,"identifyer",void 0),s([o.property({reflect:!0})],h.prototype,"color",void 0),s([o.property({reflect:!0})],h.prototype,"stroke",void 0),s([o.property({reflect:!0})],h.prototype,"fill",void 0),s([o.property({reflect:!0})],h.prototype,"size",void 0),s([o.property({reflect:!0})],h.prototype,"width",void 0),s([o.property({reflect:!0})],h.prototype,"height",void 0),s([o.queryAll(`[${a.STROKE.trackAttr}]`)],h.prototype,"strokeAppliedNodes",void 0),s([o.queryAll(`[${a.FILL.trackAttr}]`)],h.prototype,"fillAppliedNodes",void 0),e.IconparkIconElement=h,customElements.get("iconpark-icon")||customElements.define("iconpark-icon",h)}},e={};function i(s){var r=e[s];if(void 0!==r)return r.exports;var o=e[s]={exports:{}};return t[s].call(o.exports,o,o.exports,i),o.exports}i.d=(t,e)=>{for(var s in e)i.o(e,s)&&!i.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:e[s]})},i.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),i.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},i(409)})(); \ No newline at end of file diff --git a/frontend/packages/businessEntry/public/vite.svg b/frontend/packages/businessEntry/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/frontend/packages/businessEntry/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/packages/businessEntry/src/App.tsx b/frontend/packages/businessEntry/src/App.tsx new file mode 100644 index 00000000..52dac839 --- /dev/null +++ b/frontend/packages/businessEntry/src/App.tsx @@ -0,0 +1,158 @@ +import '@core/App.css' +import { ConfigProvider } from 'antd'; +import RenderRoutes from '@businessEntry/components/aoplatform/RenderRoutes'; +import {BreadcrumbProvider} from "@common/contexts/BreadcrumbContext.tsx"; +import { StyleProvider } from '@ant-design/cssinjs'; +import zhCN from 'antd/locale/zh_CN'; +import useInitializeMonaco from "@common/hooks/useInitializeMonaco"; +import ThemeSwitcher from '@common/components/aoplatform/ThemeSwitcher' + +const antdComponentThemeToken = { + token: { + // Seed Token,影响范围大 + colorPrimary: '#3D46F2', + colorLink:'#3D46F2', + colorBorder:'#ededed', + colorText:'#333', + borderRadius: 4, + // 派生变量,影响范围小 + colorBgContainer: '#fff', + colorPrimaryBg:'#EBEEF2', + colorTextQuaternary:'#BBB', + colorTextTertiary:'#999' + }, + components:{ + // 派生变量,影响范围小 + Input:{ + activeShadow:'none' + }, + Select:{ + activeShadow:'none' + }, + Checkbox:{ + activeShadow:'none' + }, + Cascader:{ + activeShadow:'none', + optionSelectedBg:'#EBEEF2', + optionHoverBg:'#EBEEF2' + }, + Layout: { + bodyBg: '#17163E', + headerBg: 'transparent', + headerColor: '#333', + headerPadding: '10 20px', + lightSiderBg: 'transparent', + siderBg: 'transparent', + }, + Breadcrumb:{ + itemColor:'#666', + linkColor:'#666', + lastItemColor:'#333', + }, + Table:{ + headerBorderRadius:0, + headerSplitColor:'#ededed', + borderColor:'#ededed', + cellPaddingBlockMD:'10px', + cellPaddingInlineMD:'12px', + cellPaddingBlockSM:'8px', + cellPaddingInlineSM:'12px', + headerFilterHoverBg:'#EBEEF2', + headerSortActiveBg:'#F7F8FA', + headerSortHoverBg:'#F7F8FA', + fixedHeaderSortActiveBg:'#F7F8FA', + headerBg:'#F7F8FA', + rowHoverBg:'#EBEEF2' + + }, + Segmented:{ + itemColor:'#333', + itemSelectedColor:'#333', + trackBg:'#f7f8fa', + trackPadding:0, + // itemHoverColor:'#EBEEF2', + itemActiveBg:'#EBEEF2', + itemHoverBg:'#EBEEF2', + itemSelectedBg:'#EBEEF2', + }, + Tree:{ + // titleHeight:30, + // fontSize:12, + directoryNodeSelectedBg:'#EBEEF2', + directoryNodeSelectedColor:'#333', + nodeSelectedBg:'#EBEEF2', + nodeHoverBg:'#EBEEF2' + }, + Collapse:{ + headerBg:'#f7f8fa', + headerPadding:"12px", + contentPadding:"0 10px 12px 10px" + }, + Button:{ + // paddingInline:8, + dangerShadow:'none', + defaultShadow:'none', + primaryShadow:'none' + }, + Tabs:{ + cardBg:'#EBEEF2', + cardHeight:42, + horizontalItemGutter:8, + horizontalItemPaddingSM:'12px 8px 8px 8px', + horizontalItemPadding:'12px 8px 8px 8px', + }, + Menu:{ + // itemBg:'#F7F8FA', + // subMenuItemBg:'#F7F8FA', + // itemMarginBlock:0, + // activeBarBorderWidth:0, + // itemSelectedColor:'#333', + // itemSelectedBg:'#EBEEF2', + // itemHoverBg:'#EBEEF2' + // itemHeight:'72px', + // darkItemBg:'transparent', + // itemBg:'transparent', + // itemSelectedBg:'transparent', + // darkItemSelectedBg:'transparent', + // subMenuItemBg:'transparent', + // itemActiveBg:'transparent', + // darkSubMenuItemBg:'transparent', + // activeBarHeight:'2px', + // activeBarBorderWidth:2 + }, + List:{ + itemPadding:'8px 0' + }, + Form:{ + itemMarginBottom:10, + + }, + Alert:{ + defaultPadding:'12px 16px' + }, + Tag:{ + defaultBg:"#f7f8fa" + }, + } +} + +function App() { + useInitializeMonaco() + + return ( + + + + + + + + + ); +} + +export default App diff --git a/frontend/packages/businessEntry/src/components/aoplatform/RenderRoutes.tsx b/frontend/packages/businessEntry/src/components/aoplatform/RenderRoutes.tsx new file mode 100644 index 00000000..69f50f4d --- /dev/null +++ b/frontend/packages/businessEntry/src/components/aoplatform/RenderRoutes.tsx @@ -0,0 +1,471 @@ +import { BrowserRouter as Router, Routes, Route, Navigate, Outlet } from 'react-router-dom'; +import Login from "@core/pages/Login.tsx" +import BasicLayout from '@common/components/aoplatform/BasicLayout'; +import {createElement, ReactElement,ReactNode,Suspense} from 'react'; +import { v4 as uuidv4 } from 'uuid' +import {App, Skeleton} from "antd"; +import ApprovalPage from "@core/pages/approval/ApprovalPage.tsx"; +import {SystemProvider} from "@core/contexts/SystemContext.tsx"; +import {useGlobalContext} from "@common/contexts/GlobalStateContext.tsx"; +import {FC,lazy} from 'react'; +import { TeamProvider } from '@core/contexts/TeamContext.tsx'; +import SystemOutlet from '@core/pages/system/SystemOutlet.tsx'; +import { DashboardProvider } from '@core/contexts/DashboardContext.tsx'; +import { PartitionProvider } from '@core/contexts/PartitionContext.tsx'; +import { TenantManagementProvider } from '@market/contexts/TenantManagementContext.tsx'; + +type RouteConfig = { + path:string + component?:ReactElement + children?:(RouteConfig|false)[] + key:string + provider?:FC<{ children: ReactNode; }> + lazy?:unknown +} +const APP_MODE = import.meta.env.VITE_APP_MODE; +export type RouterParams = { + teamId:string + apiId:string + serviceId:string + clusterId:string; + memberGroupId:string + userGroupId:string + pluginName:string + moduleId:string + accessType:'project'|'team'|'service' + categoryId:string + tagId:string + dashboardType:string + dashboardDetailId:string + topologyId:string + appId:string + roleType:string + roleId:string +} + +const PUBLIC_ROUTES:RouteConfig[] = [ + { + path:'/', + component:, + key: uuidv4(), + }, + { + path:'/login', + component:, + key: uuidv4() + }, + { + path:'/', + component:, + key: uuidv4(), + children:[ + { + path:'approval/*', + component:, + key:uuidv4() + }, + { + path:'team', + component:, + key: uuidv4(), + provider: TeamProvider, + children:[ + { + path:'', + key: uuidv4(), + component: + }, + { + path:'list', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamList.tsx')) + }, + { + path:'inside/:teamId', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamInsidePage.tsx')), + key: uuidv4(), + children:[ + { + path:'member', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamInsideMember.tsx')), + }, + { + path:'setting', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamConfig.tsx')), + }, + ] + } + ] + }, + { + path:'service', + component:, + key: uuidv4(), + provider: SystemProvider, + children:[ + { + path:'', + key:uuidv4(), + component: + }, + { + path:'list', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemList.tsx')), + }, + { + path:'list/:teamId', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemList.tsx')), + }, + { + path:':teamId', + component:, + key: uuidv4(), + children:[ + { + path:'inside/:serviceId', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemInsidePage.tsx')), + children:[ + { + path:'api', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/api/SystemInsideApiList.tsx')), + }, + { + path:'upstream', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/upstream/SystemInsideUpstreamContent.tsx')), + }, + { + path:'document', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemInsideDocument.tsx')), + }, + { + path:'subscriber', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemInsideSubscriber.tsx')), + children:[ + + ] + }, + { + path:'approval', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/approval/SystemInsideApproval.tsx')), + children:[ + { + path:'', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/approval/SystemInsideApprovalList.tsx')), + }, + { + path:'*', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/approval/SystemInsideApprovalList.tsx')), + } + ] + }, + { + path:'topology', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemTopology.tsx')), + key: uuidv4(), + children:[ + ] + }, + { + path:'publish', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/publish/SystemInsidePublish.tsx')), + children:[ + { + path:'*', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/publish/SystemInsidePublishList.tsx')), + } + ] + }, + { + path:'setting', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemConfig.tsx')), + children:[ + + ] + }, + ] + } + ] + } + ] + }, + { + path:'cluster', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/partitions/PartitionInsideCluster.tsx')), + }, + { + path:'cert', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/partitions/PartitionInsideCert.tsx')), + }, + { + path:'serviceHub', + component:, + key:uuidv4(), + children:[ + { + path:'', + key: uuidv4(), + component: + }, + { + path:'list', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/ServiceHubList.tsx')), + }, + { + path:'detail/:serviceId', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/ServiceHubDetail.tsx')), + }] + }, + { + path:'servicecategories', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/serviceCategory/ServiceCategory.tsx')), + key:uuidv4(), + }, + { + path:'tenantManagement', + component:, + provider:TenantManagementProvider, + key:uuidv4(), + children:[ + { + path:'', + key:uuidv4(), + component: + }, + { + path:':teamId/inside/:appId', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementInsidePage.tsx')), + children:[ + { + path:'service', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementInsideService.tsx')), + }, + { + path:'authorization', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementInsideAuth.tsx')), + }, + { + path:'setting', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementAppSetting.tsx')), + }, + ] + }, + { + path:'list', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ServiceHubManagement.tsx')), + }, + { + path:'list/:teamId', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ServiceHubManagement.tsx')), + }, + ] + }, + { + path:'member', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/member/MemberPage.tsx')), + children:[ + { + path:'', + key:uuidv4(), + component: + }, + { + path:'list', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/member/MemberList.tsx')), + }, + { + path:'list/:memberGroupId', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/member/MemberList.tsx')), + } + ] + }, + { + path:'role', + key:uuidv4(), + component:, + children:[ + { + path: '', + key: uuidv4(), + component: + }, + { + path:'list', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/role/RoleList.tsx')), + }, + { + path:':roleType/config', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/role/RoleConfig.tsx')), + }, + { + path:':roleType/config/:roleId', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/role/RoleConfig.tsx')), + } + ] + }, + APP_MODE === 'pro' &&{ + path:'openapi', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@openApi/pages/OpenApiList.tsx')), + key:uuidv4(), + }, + { + path:'logretrieval', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/logRetrieval/LogRetrieval.tsx')), + key:uuidv4(), + }, + { + path:'auditlog', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/auditLog/AuditLog.tsx')), + key:uuidv4(), + }, + { + path:'assets', + component:

设计中

, + key:uuidv4() + }, + APP_MODE === 'pro' &&{ + path:'dashboard', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@dashboard/pages/Dashboard.tsx')), + key:uuidv4(), + children:[ + { + path:':dashboardType', + component:, + key:uuidv4(), + provider:DashboardProvider, + children:[ + { + path:'list', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@dashboard/pages/DashboardList.tsx')), + key:uuidv4() + }, + { + path:'detail/:dashboardDetailId', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@dashboard/pages/DashboardDetail.tsx')), + key:uuidv4() + }, + ] + }, + ] + }, + { + path:'systemrunning', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@systemRunning/pages/SystemRunning.tsx')), + key:uuidv4() + }, + { + path:'template/:moduleId', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '../../../../common/src/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx')), + key:uuidv4() + }, + { + path:'logsettings/*', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/logsettings/LogSettings.tsx')), + key: uuidv4(), + children:[{ + path:'template/:moduleId', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '../../../../common/src/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx')), + key:uuidv4() + }] + + }, + APP_MODE ==='pro' && { + path:'resourcesettings/*', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/resourcesettings/ResourceSettings.tsx')), + key: uuidv4(), + children:[{ + path:'template/:moduleId', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '../../../../common/src/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx')), + key:uuidv4() + }] + + } + ] + }, +] + +const RenderRoutes = ()=> { + return ( + + + + {generateRoutes(PUBLIC_ROUTES)} + + + + ) +} + +const generateRoutes = (routerConfig: RouteConfig[]) => { + return routerConfig?.map((route: RouteConfig) => { + let routeElement; + if (route.lazy) { + const LazyComponent = route.lazy as React.ExoticComponent; + + routeElement = ( + }> + {route.provider ? ( + createElement(route.provider, {}, ) + ) : ( + + )} + + ); + } else { + routeElement = route.provider ? ( + createElement(route.provider, {}, route.component) + ) : ( + route.component + ); + } + + return ( + + {route.children && generateRoutes(route.children as RouteConfig[])} + + ); + } + ) +} + +// 保护的路由组件 +function ProtectedRoute() { + const {state} = useGlobalContext() + return state.isAuthenticated? : ; + } + +export default RenderRoutes \ No newline at end of file diff --git a/frontend/packages/businessEntry/src/main.tsx b/frontend/packages/businessEntry/src/main.tsx new file mode 100644 index 00000000..cfb7d8ef --- /dev/null +++ b/frontend/packages/businessEntry/src/main.tsx @@ -0,0 +1,27 @@ +import {StrictMode} from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import '@core/index.css' +import {GlobalProvider} from "@common/contexts/GlobalStateContext.tsx"; + +async function initializeApp() { + try { + // 初始化行为 + // await fetchInitialConfig(); // 示例:获取初始配置 + + // 异步操作完成后,渲染React应用 + ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + , + ); + } catch (error) { + console.error('Initialization failed:', error); + // 处理初始化失败的情况,比如渲染一个错误界面 + } +} + +// 执行初始化 +initializeApp(); \ No newline at end of file diff --git a/frontend/packages/businessEntry/src/vite-env.d.ts b/frontend/packages/businessEntry/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/frontend/packages/businessEntry/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/packages/businessEntry/start-vite.js b/frontend/packages/businessEntry/start-vite.js new file mode 100644 index 00000000..f55d1cb4 --- /dev/null +++ b/frontend/packages/businessEntry/start-vite.js @@ -0,0 +1,22 @@ +/* + * @Date: 2024-06-05 09:35:25 + * @LastEditors: maggieyyy + * @LastEditTime: 2024-06-05 10:50:12 + * @FilePath: \frontend\packages\core\start-vite.js + */ +// start-vite.js// start-vite.js +import { exec } from 'child_process'; + +const viteProcess = exec('pnpm run build'); + +viteProcess.stdout.on('data', (data) => { + console.log(data.toString()); +}); + +viteProcess.stderr.on('data', (data) => { + console.error(data.toString()); +}); + +viteProcess.on('close', (code) => { + console.log(`Vite process exited with code ${code}`); +}); diff --git a/frontend/packages/businessEntry/tsconfig.json b/frontend/packages/businessEntry/tsconfig.json new file mode 100644 index 00000000..4bc7c38f --- /dev/null +++ b/frontend/packages/businessEntry/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + /* Linting */ + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "paths": { + "@core/*": ["../core/src/*"], + "@common/*": ["../common/src/*"], + "@market/*": ["../market/src/*"], + "@dashboard/*": ["../dashboard/src/*"], + "@openApi/*": ["../openApi/src/*"], + "@systemRunning/*": ["../systemRunning/src/*"], + "@businessEntry/*": ["./src/*"], + }, + }, + "include": ["src", "public/iconpark_eolink.js", "public/iconpark_apinto.js", "../common/src/component/aoplatform/EditableTableWithModal.tsx", "../common/src/components/aoplatform/TransferTable.tsx", "../common/src/components/aoplatform/TreeWithMore.tsx", "../common/src/components/aoplatform/DatePicker.tsx", "../common/src/components/aoplatform/TimeRangeSelector.tsx", "../common/src/components/aoplatform/TimePicker.tsx", "../common/src/components/aoplatform/MemberTransfer.tsx", "../common/src/components/aoplatform/Navigation.tsx", "../common/src/components/aoplatform/PageList.tsx", "../common/src/components/aoplatform/GroupTree.tsx", "../common/src/components/aoplatform/ErrorBoundary.tsx", "../common/src/components/aoplatform/ScrollableSection.tsx", "../common/src/utils/postcat.tsx", "../common/src/utils/curl.ts", "../common/src/components/aoplatform/ResetPsw.tsx", "../common/src/components/aoplatform/SubscribeApprovalModalContent.tsx", "../common/src/components/aoplatform/InsidePageForHub.tsx", "src/components/aoplatform/RenderRoutes.tsx", "../common/src/components/aoplatform/PublishApprovalModalContent.tsx", "../common/src/components/aoplatform/InsidePage.tsx", "../common/src/const/type.ts", "../common/src/components/aoplatform/intelligent-plugin", "../common/src/const/domain"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/packages/businessEntry/tsconfig.node.json b/frontend/packages/businessEntry/tsconfig.node.json new file mode 100644 index 00000000..42872c59 --- /dev/null +++ b/frontend/packages/businessEntry/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/packages/businessEntry/vite.config.ts b/frontend/packages/businessEntry/vite.config.ts new file mode 100644 index 00000000..895d5197 --- /dev/null +++ b/frontend/packages/businessEntry/vite.config.ts @@ -0,0 +1,80 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' +import dynamicImportVars from '@rollup/plugin-dynamic-import-vars'; +import tailwindcss from 'tailwindcss'; +import autoprefixer from 'autoprefixer'; + +export default defineConfig({ + cacheDir: './node_modules/.vite', + build:{ + outDir:'../../dist', + sourcemap: false, + chunkSizeWarningLimit: 50000, + cacheDir: './node_modules/.vite', + output: { + manualChunks(id) { + if (id.includes('node_modules')) { + return id.toString().split('node_modules/')[1].split('/')[0].toString(); + } + // 针对 pnpm 和 Monorepo 特殊处理 + if (id.includes('.pnpm')) { + const segments = id.split(path.sep); + const packageName = segments[segments.indexOf('.pnpm') + 1].split('@')[0]; + return packageName; + } + } + }, + }, + css: { + postcss: { + plugins: [ + tailwindcss(path.resolve(__dirname, '../common/tailwind.config.js')), + autoprefixer + ], + }, + preprocessorOptions: { + less: { + javascriptEnabled: true, + }, + }, + modules:{ + localsConvention:"camelCase", + generateScopedName:"[local]_[hash:base64:2]" + } + }, + plugins: [react(), + dynamicImportVars({ + include:["src"], + exclude:[], + warnOnError:false + }), + ], + resolve: { + alias: [ + { find: /^~/, replacement: '' }, + { find: '@common', replacement: path.resolve(__dirname, '../common/src') }, + { find: '@market', replacement: path.resolve(__dirname, '../market/src') }, + { find: '@core', replacement: path.resolve(__dirname, '../core/src') }, + { find: '@dashboard', replacement: path.resolve(__dirname, '../dashboard/src') }, + { find: '@openApi', replacement: path.resolve(__dirname, '../openApi/src') }, + { find: '@systemRunning', replacement: path.resolve(__dirname, '../systemRunning/src') }, + { find: '@businessEntry', replacement: path.resolve(__dirname, './src') }, + ] + }, + server: { + proxy: { + '/api/v1': { + // target: 'http://uat.apikit.com:11204/mockApi/aoplatform/', + target: 'http://172.18.166.219:8288/', + changeOrigin: true, + }, + '/api2/v1': { + // target: 'http://uat.apikit.com:11204/mockApi/aoplatform/', + target: 'http://172.18.166.219:8288/', + changeOrigin: true, + } + } + }, + logLevel:'info' +}) diff --git a/frontend/packages/businessEntry/vite.config.ts.timestamp-1722480612803-56fa594878983.mjs b/frontend/packages/businessEntry/vite.config.ts.timestamp-1722480612803-56fa594878983.mjs new file mode 100644 index 00000000..b2f55614 --- /dev/null +++ b/frontend/packages/businessEntry/vite.config.ts.timestamp-1722480612803-56fa594878983.mjs @@ -0,0 +1,85 @@ +// vite.config.ts +import { defineConfig } from "file:///D:/eolink/applatform/frontend/node_modules/.pnpm/vite@5.2.12_@types+node@20.14.2_less@4.2.0/node_modules/vite/dist/node/index.js"; +import react from "file:///D:/eolink/applatform/frontend/node_modules/.pnpm/@vitejs+plugin-react@4.3.0_vite@5.2.12_@types+node@20.14.2_less@4.2.0_/node_modules/@vitejs/plugin-react/dist/index.mjs"; +import path from "path"; +import dynamicImportVars from "file:///D:/eolink/applatform/frontend/node_modules/.pnpm/@rollup+plugin-dynamic-import-vars@2.1.2_rollup@4.18.0/node_modules/@rollup/plugin-dynamic-import-vars/dist/es/index.js"; +import tailwindcss from "file:///D:/eolink/applatform/frontend/node_modules/.pnpm/tailwindcss@3.4.4/node_modules/tailwindcss/lib/index.js"; +import autoprefixer from "file:///D:/eolink/applatform/frontend/node_modules/.pnpm/autoprefixer@10.4.19_postcss@8.4.38/node_modules/autoprefixer/lib/autoprefixer.js"; +var __vite_injected_original_dirname = "D:\\eolink\\applatform\\frontend\\packages\\businessEntry"; +var vite_config_default = defineConfig({ + cacheDir: "./node_modules/.vite", + build: { + outDir: "../../dist", + sourcemap: false, + chunkSizeWarningLimit: 5e4, + cacheDir: "./node_modules/.vite", + output: { + manualChunks(id) { + if (id.includes("node_modules")) { + return id.toString().split("node_modules/")[1].split("/")[0].toString(); + } + if (id.includes(".pnpm")) { + const segments = id.split(path.sep); + const packageName = segments[segments.indexOf(".pnpm") + 1].split("@")[0]; + return packageName; + } + } + } + }, + css: { + postcss: { + plugins: [ + tailwindcss(path.resolve(__vite_injected_original_dirname, "../common/tailwind.config.js")), + autoprefixer + ] + }, + preprocessorOptions: { + less: { + javascriptEnabled: true + } + }, + modules: { + localsConvention: "camelCase", + generateScopedName: "[local]_[hash:base64:2]" + } + }, + plugins: [ + react(), + dynamicImportVars({ + include: ["src"], + exclude: [], + warnOnError: false + }) + ], + resolve: { + alias: [ + { find: /^~/, replacement: "" }, + { find: "@common", replacement: path.resolve(__vite_injected_original_dirname, "../common/src") }, + { find: "@market", replacement: path.resolve(__vite_injected_original_dirname, "../market/src") }, + { find: "@core", replacement: path.resolve(__vite_injected_original_dirname, "../core/src") }, + { find: "@dashboard", replacement: path.resolve(__vite_injected_original_dirname, "../dashboard/src") }, + { find: "@openApi", replacement: path.resolve(__vite_injected_original_dirname, "../openApi/src") }, + { find: "@systemRunning", replacement: path.resolve(__vite_injected_original_dirname, "../systemRunning/src") }, + { find: "@businessEntry", replacement: path.resolve(__vite_injected_original_dirname, "./src") } + ] + }, + server: { + proxy: { + "/api/v1": { + // target: 'http://uat.apikit.com:11204/mockApi/aoplatform/', + target: "http://172.18.166.219:8288/", + changeOrigin: true + }, + "/api2/v1": { + // target: 'http://uat.apikit.com:11204/mockApi/aoplatform/', + target: "http://172.18.166.219:8288/", + changeOrigin: true + } + } + }, + logLevel: "info" +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJEOlxcXFxlb2xpbmtcXFxcYXBwbGF0Zm9ybVxcXFxmcm9udGVuZFxcXFxwYWNrYWdlc1xcXFxidXNpbmVzc0VudHJ5XCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCJEOlxcXFxlb2xpbmtcXFxcYXBwbGF0Zm9ybVxcXFxmcm9udGVuZFxcXFxwYWNrYWdlc1xcXFxidXNpbmVzc0VudHJ5XFxcXHZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9EOi9lb2xpbmsvYXBwbGF0Zm9ybS9mcm9udGVuZC9wYWNrYWdlcy9idXNpbmVzc0VudHJ5L3ZpdGUuY29uZmlnLnRzXCI7LypcclxuICogQERhdGU6IDIwMjQtMDEtMzEgMTU6MDA6MzlcclxuICogQExhc3RFZGl0b3JzOiBtYWdnaWV5eXlcclxuICogQExhc3RFZGl0VGltZTogMjAyNC0wOC0wMSAxMDo1MDoxMlxyXG4gKiBARmlsZVBhdGg6IFxcZnJvbnRlbmRcXHBhY2thZ2VzXFxidXNpbmVzc0VudHJ5XFx2aXRlLmNvbmZpZy50c1xyXG4gKi9cclxuaW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZSdcclxuaW1wb3J0IHJlYWN0IGZyb20gJ0B2aXRlanMvcGx1Z2luLXJlYWN0J1xyXG5pbXBvcnQgcGF0aCBmcm9tICdwYXRoJ1xyXG5pbXBvcnQgZHluYW1pY0ltcG9ydFZhcnMgZnJvbSAnQHJvbGx1cC9wbHVnaW4tZHluYW1pYy1pbXBvcnQtdmFycyc7XHJcbmltcG9ydCB0YWlsd2luZGNzcyBmcm9tICd0YWlsd2luZGNzcyc7XHJcbmltcG9ydCBhdXRvcHJlZml4ZXIgZnJvbSAnYXV0b3ByZWZpeGVyJztcclxuXHJcbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XHJcbiAgY2FjaGVEaXI6ICcuL25vZGVfbW9kdWxlcy8udml0ZScsXHJcbiAgYnVpbGQ6e1xyXG4gICAgb3V0RGlyOicuLi8uLi9kaXN0JyxcclxuICAgIHNvdXJjZW1hcDogZmFsc2UsXHJcbiAgICBjaHVua1NpemVXYXJuaW5nTGltaXQ6IDUwMDAwLFxyXG4gICAgY2FjaGVEaXI6ICcuL25vZGVfbW9kdWxlcy8udml0ZScsIFxyXG4gICAgICBvdXRwdXQ6IHtcclxuICAgICAgICBtYW51YWxDaHVua3MoaWQpIHtcclxuICAgICAgICAgIGlmIChpZC5pbmNsdWRlcygnbm9kZV9tb2R1bGVzJykpIHtcclxuICAgICAgICAgICAgcmV0dXJuIGlkLnRvU3RyaW5nKCkuc3BsaXQoJ25vZGVfbW9kdWxlcy8nKVsxXS5zcGxpdCgnLycpWzBdLnRvU3RyaW5nKCk7XHJcbiAgICAgICAgICB9XHJcbiAgICAgICAgICAvLyBcdTk0ODhcdTVCRjkgcG5wbSBcdTU0OEMgTW9ub3JlcG8gXHU3Mjc5XHU2QjhBXHU1OTA0XHU3NDA2XHJcbiAgICAgICAgICBpZiAoaWQuaW5jbHVkZXMoJy5wbnBtJykpIHtcclxuICAgICAgICAgICAgY29uc3Qgc2VnbWVudHMgPSBpZC5zcGxpdChwYXRoLnNlcCk7XHJcbiAgICAgICAgICAgIGNvbnN0IHBhY2thZ2VOYW1lID0gc2VnbWVudHNbc2VnbWVudHMuaW5kZXhPZignLnBucG0nKSArIDFdLnNwbGl0KCdAJylbMF07XHJcbiAgICAgICAgICAgIHJldHVybiBwYWNrYWdlTmFtZTtcclxuICAgICAgICAgIH1cclxuICAgICAgICB9XHJcbiAgICAgIH0sXHJcbiAgICB9LFxyXG4gIGNzczoge1xyXG4gICAgcG9zdGNzczoge1xyXG4gICAgICBwbHVnaW5zOiBbXHJcbiAgICAgICAgdGFpbHdpbmRjc3MocGF0aC5yZXNvbHZlKF9fZGlybmFtZSwgJy4uL2NvbW1vbi90YWlsd2luZC5jb25maWcuanMnKSksIFxyXG4gICAgICAgIGF1dG9wcmVmaXhlclxyXG4gICAgICBdLFxyXG4gICAgfSxcclxuICAgIHByZXByb2Nlc3Nvck9wdGlvbnM6IHtcclxuICAgICAgbGVzczoge1xyXG4gICAgICAgIGphdmFzY3JpcHRFbmFibGVkOiB0cnVlLFxyXG4gICAgICB9LFxyXG4gICAgfSxcclxuICAgIG1vZHVsZXM6e1xyXG4gICAgICBsb2NhbHNDb252ZW50aW9uOlwiY2FtZWxDYXNlXCIsXHJcbiAgICAgIGdlbmVyYXRlU2NvcGVkTmFtZTpcIltsb2NhbF1fW2hhc2g6YmFzZTY0OjJdXCJcclxuICAgIH1cclxuICB9LFxyXG4gIHBsdWdpbnM6IFtyZWFjdCgpLFxyXG4gICAgICBkeW5hbWljSW1wb3J0VmFycyh7XHJcbiAgICAgICAgaW5jbHVkZTpbXCJzcmNcIl0sXHJcbiAgICAgICAgZXhjbHVkZTpbXSxcclxuICAgICAgICB3YXJuT25FcnJvcjpmYWxzZVxyXG4gICAgICAgfSksXHJcbiAgICBdLFxyXG4gIHJlc29sdmU6IHtcclxuICAgIGFsaWFzOiBbXHJcbiAgICAgIHsgZmluZDogL15+LywgcmVwbGFjZW1lbnQ6ICcnIH0sXHJcbiAgICAgIHsgZmluZDogJ0Bjb21tb24nLCByZXBsYWNlbWVudDogcGF0aC5yZXNvbHZlKF9fZGlybmFtZSwgJy4uL2NvbW1vbi9zcmMnKSB9LFxyXG4gICAgICB7IGZpbmQ6ICdAbWFya2V0JywgcmVwbGFjZW1lbnQ6IHBhdGgucmVzb2x2ZShfX2Rpcm5hbWUsICcuLi9tYXJrZXQvc3JjJykgfSxcclxuICAgICAgeyBmaW5kOiAnQGNvcmUnLCByZXBsYWNlbWVudDogcGF0aC5yZXNvbHZlKF9fZGlybmFtZSwgJy4uL2NvcmUvc3JjJykgfSxcclxuICAgICAgeyBmaW5kOiAnQGRhc2hib2FyZCcsIHJlcGxhY2VtZW50OiBwYXRoLnJlc29sdmUoX19kaXJuYW1lLCAnLi4vZGFzaGJvYXJkL3NyYycpIH0sXHJcbiAgICAgIHsgZmluZDogJ0BvcGVuQXBpJywgcmVwbGFjZW1lbnQ6IHBhdGgucmVzb2x2ZShfX2Rpcm5hbWUsICcuLi9vcGVuQXBpL3NyYycpIH0sXHJcbiAgICAgIHsgZmluZDogJ0BzeXN0ZW1SdW5uaW5nJywgcmVwbGFjZW1lbnQ6IHBhdGgucmVzb2x2ZShfX2Rpcm5hbWUsICcuLi9zeXN0ZW1SdW5uaW5nL3NyYycpIH0sXHJcbiAgICAgIHsgZmluZDogJ0BidXNpbmVzc0VudHJ5JywgcmVwbGFjZW1lbnQ6IHBhdGgucmVzb2x2ZShfX2Rpcm5hbWUsICcuL3NyYycpIH0sXHJcbiAgICBdXHJcbiAgfSxcclxuICBzZXJ2ZXI6IHtcclxuICAgIHByb3h5OiB7XHJcbiAgICAgICcvYXBpL3YxJzoge1xyXG4gICAgICAgIC8vIHRhcmdldDogJ2h0dHA6Ly91YXQuYXBpa2l0LmNvbToxMTIwNC9tb2NrQXBpL2FvcGxhdGZvcm0vJyxcclxuICAgICAgICB0YXJnZXQ6ICdodHRwOi8vMTcyLjE4LjE2Ni4yMTk6ODI4OC8nLFxyXG4gICAgICAgIGNoYW5nZU9yaWdpbjogdHJ1ZSxcclxuICAgICAgfSxcclxuICAgICAgJy9hcGkyL3YxJzoge1xyXG4gICAgICAgIC8vIHRhcmdldDogJ2h0dHA6Ly91YXQuYXBpa2l0LmNvbToxMTIwNC9tb2NrQXBpL2FvcGxhdGZvcm0vJyxcclxuICAgICAgICB0YXJnZXQ6ICdodHRwOi8vMTcyLjE4LjE2Ni4yMTk6ODI4OC8nLFxyXG4gICAgICAgIGNoYW5nZU9yaWdpbjogdHJ1ZSxcclxuICAgICAgfVxyXG4gICAgfVxyXG4gIH0sXHJcbiAgbG9nTGV2ZWw6J2luZm8nXHJcbn0pXHJcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFNQSxTQUFTLG9CQUFvQjtBQUM3QixPQUFPLFdBQVc7QUFDbEIsT0FBTyxVQUFVO0FBQ2pCLE9BQU8sdUJBQXVCO0FBQzlCLE9BQU8saUJBQWlCO0FBQ3hCLE9BQU8sa0JBQWtCO0FBWHpCLElBQU0sbUNBQW1DO0FBYXpDLElBQU8sc0JBQVEsYUFBYTtBQUFBLEVBQzFCLFVBQVU7QUFBQSxFQUNWLE9BQU07QUFBQSxJQUNKLFFBQU87QUFBQSxJQUNQLFdBQVc7QUFBQSxJQUNYLHVCQUF1QjtBQUFBLElBQ3ZCLFVBQVU7QUFBQSxJQUNSLFFBQVE7QUFBQSxNQUNOLGFBQWEsSUFBSTtBQUNmLFlBQUksR0FBRyxTQUFTLGNBQWMsR0FBRztBQUMvQixpQkFBTyxHQUFHLFNBQVMsRUFBRSxNQUFNLGVBQWUsRUFBRSxDQUFDLEVBQUUsTUFBTSxHQUFHLEVBQUUsQ0FBQyxFQUFFLFNBQVM7QUFBQSxRQUN4RTtBQUVBLFlBQUksR0FBRyxTQUFTLE9BQU8sR0FBRztBQUN4QixnQkFBTSxXQUFXLEdBQUcsTUFBTSxLQUFLLEdBQUc7QUFDbEMsZ0JBQU0sY0FBYyxTQUFTLFNBQVMsUUFBUSxPQUFPLElBQUksQ0FBQyxFQUFFLE1BQU0sR0FBRyxFQUFFLENBQUM7QUFDeEUsaUJBQU87QUFBQSxRQUNUO0FBQUEsTUFDRjtBQUFBLElBQ0Y7QUFBQSxFQUNGO0FBQUEsRUFDRixLQUFLO0FBQUEsSUFDSCxTQUFTO0FBQUEsTUFDUCxTQUFTO0FBQUEsUUFDUCxZQUFZLEtBQUssUUFBUSxrQ0FBVyw4QkFBOEIsQ0FBQztBQUFBLFFBQ25FO0FBQUEsTUFDRjtBQUFBLElBQ0Y7QUFBQSxJQUNBLHFCQUFxQjtBQUFBLE1BQ25CLE1BQU07QUFBQSxRQUNKLG1CQUFtQjtBQUFBLE1BQ3JCO0FBQUEsSUFDRjtBQUFBLElBQ0EsU0FBUTtBQUFBLE1BQ04sa0JBQWlCO0FBQUEsTUFDakIsb0JBQW1CO0FBQUEsSUFDckI7QUFBQSxFQUNGO0FBQUEsRUFDQSxTQUFTO0FBQUEsSUFBQyxNQUFNO0FBQUEsSUFDWixrQkFBa0I7QUFBQSxNQUNoQixTQUFRLENBQUMsS0FBSztBQUFBLE1BQ2QsU0FBUSxDQUFDO0FBQUEsTUFDVCxhQUFZO0FBQUEsSUFDYixDQUFDO0FBQUEsRUFDSjtBQUFBLEVBQ0YsU0FBUztBQUFBLElBQ1AsT0FBTztBQUFBLE1BQ0wsRUFBRSxNQUFNLE1BQU0sYUFBYSxHQUFHO0FBQUEsTUFDOUIsRUFBRSxNQUFNLFdBQVcsYUFBYSxLQUFLLFFBQVEsa0NBQVcsZUFBZSxFQUFFO0FBQUEsTUFDekUsRUFBRSxNQUFNLFdBQVcsYUFBYSxLQUFLLFFBQVEsa0NBQVcsZUFBZSxFQUFFO0FBQUEsTUFDekUsRUFBRSxNQUFNLFNBQVMsYUFBYSxLQUFLLFFBQVEsa0NBQVcsYUFBYSxFQUFFO0FBQUEsTUFDckUsRUFBRSxNQUFNLGNBQWMsYUFBYSxLQUFLLFFBQVEsa0NBQVcsa0JBQWtCLEVBQUU7QUFBQSxNQUMvRSxFQUFFLE1BQU0sWUFBWSxhQUFhLEtBQUssUUFBUSxrQ0FBVyxnQkFBZ0IsRUFBRTtBQUFBLE1BQzNFLEVBQUUsTUFBTSxrQkFBa0IsYUFBYSxLQUFLLFFBQVEsa0NBQVcsc0JBQXNCLEVBQUU7QUFBQSxNQUN2RixFQUFFLE1BQU0sa0JBQWtCLGFBQWEsS0FBSyxRQUFRLGtDQUFXLE9BQU8sRUFBRTtBQUFBLElBQzFFO0FBQUEsRUFDRjtBQUFBLEVBQ0EsUUFBUTtBQUFBLElBQ04sT0FBTztBQUFBLE1BQ0wsV0FBVztBQUFBO0FBQUEsUUFFVCxRQUFRO0FBQUEsUUFDUixjQUFjO0FBQUEsTUFDaEI7QUFBQSxNQUNBLFlBQVk7QUFBQTtBQUFBLFFBRVYsUUFBUTtBQUFBLFFBQ1IsY0FBYztBQUFBLE1BQ2hCO0FBQUEsSUFDRjtBQUFBLEVBQ0Y7QUFBQSxFQUNBLFVBQVM7QUFDWCxDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo= diff --git a/frontend/packages/common/README.md b/frontend/packages/common/README.md new file mode 100644 index 00000000..69371903 --- /dev/null +++ b/frontend/packages/common/README.md @@ -0,0 +1,11 @@ +# `common` + +> TODO: description + +## Usage + +``` +const common = require('common'); + +// TODO: DEMONSTRATE API +``` diff --git a/frontend/packages/common/__tests__/common.test.js b/frontend/packages/common/__tests__/common.test.js new file mode 100644 index 00000000..251784f1 --- /dev/null +++ b/frontend/packages/common/__tests__/common.test.js @@ -0,0 +1,7 @@ +'use strict'; + +const common = require('..'); +const assert = require('assert').strict; + +assert.strictEqual(common(), 'Hello from common'); +console.info('common tests passed'); diff --git a/frontend/packages/common/package.json b/frontend/packages/common/package.json new file mode 100644 index 00000000..619c752a --- /dev/null +++ b/frontend/packages/common/package.json @@ -0,0 +1,32 @@ +{ + "name": "common", + "version": "1.0.0", + "description": "Common library for AO Platform", + "scripts": { + "dev": "vite", + "build": "vite build", + "test": "node ./__tests__/common.test.js" + }, + "dependencies": { + "@formkit/auto-animate": "^0.8.1", + "@mui/icons-material": "^5.15.6", + "@mui/lab": "5.0.0-alpha.150", + "@mui/material": "5.14.14", + "@mui/x-data-grid-pro": "6.18.1", + "allotment": "^1.20.0", + "echarts": "^5.5.0", + "mockjs": "^1.1.0", + "rc-picker": "^4.1.1", + "react-dropzone": "^14.2.3", + "react-hook-form": "^7.49.3" + }, + "devDependencies": { + "@formily/antd-v5": "^1.2.1", + "@formily/core": "^2.2.13", + "@formily/react": "^2.2.13", + "@formily/reactive": "^2.2.13", + "@monaco-editor/react": "^4.6.0", + "exceljs": "^4.4.0", + "monaco-editor": "^0.45.0" + } +} diff --git a/frontend/packages/common/postcss.config.js b/frontend/packages/common/postcss.config.js new file mode 100644 index 00000000..80393090 --- /dev/null +++ b/frontend/packages/common/postcss.config.js @@ -0,0 +1,15 @@ +/* + * @Date: 2023-11-27 17:31:54 + * @LastEditors: maggieyyy + * @LastEditTime: 2023-11-29 15:49:05 + * @FilePath: \applatform\frontend\packages\core\postcss.config.js + */ +export default { + plugins: { + 'postcss-import': {}, + 'tailwindcss/nesting': {}, + tailwindcss: {}, + autoprefixer: {} + }, + } + \ No newline at end of file diff --git a/frontend/packages/common/src/assets/avatar_default.svg b/frontend/packages/common/src/assets/avatar_default.svg new file mode 100644 index 00000000..44331dce --- /dev/null +++ b/frontend/packages/common/src/assets/avatar_default.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/frontend/packages/common/src/assets/layout-logo.png b/frontend/packages/common/src/assets/layout-logo.png new file mode 100644 index 00000000..f60191cc Binary files /dev/null and b/frontend/packages/common/src/assets/layout-logo.png differ diff --git a/frontend/packages/common/src/assets/logo.png b/frontend/packages/common/src/assets/logo.png new file mode 100644 index 00000000..a6895b8c Binary files /dev/null and b/frontend/packages/common/src/assets/logo.png differ diff --git a/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx b/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx new file mode 100644 index 00000000..256f4eaa --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx @@ -0,0 +1,271 @@ +import { + ConfigProvider, + Dropdown, + MenuProps, + App} from 'antd'; +import Logo from '@common/assets/layout-logo.png'; +import AvatarPic from '@common/assets/avatar_default.svg' +import { routerKeyMap, TOTAL_MENU_ITEMS } from "./Navigation"; +import {Outlet, useLocation, useNavigate} from "react-router-dom"; +import {useEffect, useMemo, useRef, useState} from "react"; +import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'; +import { PERMISSION_DEFINITION } from '@common/const/permissions.ts'; + import { + ProConfigProvider, + ProLayout, + } from '@ant-design/pro-components'; +import { UserProfile } from './UserProfile.tsx'; +import { ResetPsw, ResetPswHandle } from './ResetPsw.tsx'; +import { BasicResponse, STATUS_CODE } from '@common/const/const.ts'; +import { UserInfoType, UserProfileHandle } from '@common/const/type.ts'; +import { useFetch } from '@common/hooks/http.ts'; + +const themeToken = { + bgLayout:'#17163E;', + header: { + heightLayoutHeader:72 + }, + pageContainer:{ + paddingBlockPageContainerContent:0, + paddingInlinePageContainerContent:0, + } +} + + function BasicLayout({project = 'core'}:{project:string}){ + const navigator = useNavigate() + const location = useLocation() + const currentUrl = location.pathname + const { accessData,checkPermission} = useGlobalContext() + const [pathname, setPathname] = useState(currentUrl); + const mainPage = project === 'core' ?'/service/list':'/serviceHub/list' + + useEffect(() => { + if(currentUrl === '/'){ + navigator(mainPage) + } + + }, [currentUrl]); + + const headerMenuData = useMemo(() => { + // 判断权限 + const hasAccess = (access: unknown) => checkPermission(access as keyof typeof PERMISSION_DEFINITION[0]); + + // 过滤菜单项 + const filterMenu = (menu: Array<{ [k: string]: unknown }>) => { + return [...menu] + .filter(x => x) // 过滤掉空数据 + .map((item: any) => { + if (item.routes && item.routes.length > 0) { + // 递归处理子菜单 + const filteredRoutes: Array<{ [k: string]: unknown }> = filterMenu(item.routes); + + if(filteredRoutes.length === 0){ + return false + } + return {...item, routes: filteredRoutes}; + } + // 处理没有 routes 的菜单项 + if (item.access) { + return hasAccess(item.access) ? item : null; + } + + // 如果没有 access 和 routes,则保留 + return item; + }) + .filter(x => x); // 过滤掉处理后为 null 的项 + }; + + // 初始过滤操作 + const res = [...TOTAL_MENU_ITEMS]!.filter(x => x).map((x: any) => (x.routes ? { ...x, routes: filterMenu(x.routes) } : x)); + // 返回处理后的数据 + return { path: '/', routes: res.map(x=> ({...x, routes: x.routes?.filter(x=> (x.access || x.routes?.length > 0))})).filter(x=> (x.access || x.routes?.length > 0)) }; + }, [accessData]); + + const { modal,message } = App.useApp() + const { dispatch,resetAccess,getGlobalAccessData} = useGlobalContext() + const [userInfo,setUserInfo] = useState() + const resetPswRef = useRef(null) + const userProfileRef = useRef(null) + const {fetchData} = useFetch() + const navigate = useNavigate(); + + const getUserInfo = ()=>{ + fetchData>('account/profile',{method:'GET'}) + .then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setUserInfo(data.profile) + dispatch({type:'UPDATE_USERDATA',userData:data.profile}) + }else{ + message.error(msg || '操作失败') + } + }) + } + + useEffect(() => { + getUserInfo() + getGlobalAccessData() + }, []); + + const logOut = ()=>{ + fetchData>('account/logout',{method:'GET'}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + dispatch({type:'LOGOUT'}) + resetAccess() + message.success(msg || '退出成功,将跳转至登录页') + navigate('/login') + }else{ + message.error(msg ||'操作失败') + } + }) + } + + const items: MenuProps['items'] = [ + { + key: '3', + label: ( + + 退出登录 + + ), + }, + ]; + + const openModal = (type:'userSetting'|'resetPsw')=>{ + let title:string = '' + let content:string|React.ReactNode = '' + switch (type){ + case 'userSetting': + title='用户设置' + content= + break; + case 'resetPsw': + title='重置密码' + content= + break; + } + modal.confirm({ + title, + content, + onOk:()=>{ + switch (type){ + case 'userSetting': + return userProfileRef.current?.save().then((res)=>{if(res === true) getUserInfo()}) + case 'resetPsw': + return resetPswRef.current?.save().then((res)=>{if(res === true) logOut()}) + } + }, + width:600, + okText:'确认', + cancelText:'取消', + closable:true, + icon:<>, + }) + } + + return( +
+ + { + return document.getElementById('test-pro-layout') || document.body; + }} + > + { + return ( + +
{dom} +
+
+ ); + }, + }} + // actionsRender={(props) => { + // if (props.isMobile) return []; + // if (typeof window === 'undefined') return []; + // return [ + // + // ]; + // }} + headerTitleRender={() => ( +
+ navigator(mainPage)} + /> +
+ )} + logo={Logo} + pageTitleRender={()=>'APIPark - 企业API数据开放平台'} + menuFooterRender={(props) => { + if (props?.collapsed) return undefined; + }} + menuItemRender={(item, dom) => ( +
{ + // 同级目录点击无效 + if(item.key && routerKeyMap.get(item.key) && routerKeyMap.get(item.key).length > 0 && routerKeyMap.get(item.key)?.indexOf(pathname.split('/')[1]) !== -1){ + return + } + if(item.key === pathname.split('/')[1]){ + return + } + + if(item.path){ + navigator(item.path) + } + setPathname(item.path || ''); + }} + > + {dom} +
+ )} + fixSiderbar={true} + layout='mix' + splitMenus={true} + collapsed={false} + collapsedButtonRender={false} + > +
+ +
+
+
+
+
+ ) +} +export default BasicLayout \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/Breadcrumb.tsx b/frontend/packages/common/src/components/aoplatform/Breadcrumb.tsx new file mode 100644 index 00000000..a375cd44 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/Breadcrumb.tsx @@ -0,0 +1,15 @@ +import { Breadcrumb } from "antd" +import { useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import {FC,useEffect} from "react"; + + +const TopBreadcrumb: FC = () => { + const { breadcrumb } = useBreadcrumb() + useEffect(() => { + }, [breadcrumb]); + return ( + + ) +} + +export default TopBreadcrumb \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/CodePage.tsx b/frontend/packages/common/src/components/aoplatform/CodePage.tsx new file mode 100644 index 00000000..15794b97 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/CodePage.tsx @@ -0,0 +1,124 @@ +import { FC } from 'react'; +import { Table } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; + +interface DataType { + httpStatusCode: string; + systemStatusCode: string; + description: string; + +} + +const columns: ColumnsType = [ + { + title: 'HTTP 状态码', + dataIndex: 'httpStatusCode', + key: 'httpStatusCode', + }, + { + title: '系统状态码', + dataIndex: 'systemStatusCode', + key: 'systemStatusCode', + }, + { + title: '描述', + dataIndex: 'description', + key: 'description', + ellipsis:true + }, + +]; + +const data: DataType[] = [ + // { + // httpStatusCode: '416', + // systemStatusCode: '10001', + // description: '尚未购买该 API 或 API 调用次数已用完', + // }, + // { + // httpStatusCode: '401', + // systemStatusCode: '10002', + // description: 'Header 参数中找不到 X-APISpace-Token 或 X-APISpace-Token 非法', + // }, + { + httpStatusCode: '413', + systemStatusCode: '10003', + description: '请求频率过高', + }, + { + httpStatusCode: '403', + systemStatusCode: '10004', + description: '请求来源非法,不在白名单中', + }, + // { + // httpStatusCode: '416', + // systemStatusCode: '10005', + // description: '该接口超 90 天未完成企业认证,请尽快于平台内完成认证', + // }, + { + httpStatusCode: '504', + systemStatusCode: '10006', + description: '网关超时', + }, + // { + // httpStatusCode: '504', + // systemStatusCode: '10006', + // description: '网关超时,请联系 APISpace 客服', + // }, + { + httpStatusCode: '404', + systemStatusCode: '10007', + description: '接口不存在', + }, + // { + // httpStatusCode: '416', + // systemStatusCode: '10008', + // description: '内部错误,请联系 APISpace 技术支持', + // }, + // { + // httpStatusCode: '401', + // systemStatusCode: '10009', + // description: 'Header 参数中找不到 Authorization-Type 或 Authorization-Type 非法', + // }, + { + httpStatusCode: '400', + systemStatusCode: '10010', + description: '无法识别请求内容,请检查请求体是否正确', + }, + { + httpStatusCode: '400', + systemStatusCode: '10011', + description: '请求头部缺少 Content-Type 字段', + }, + { + httpStatusCode: '400', + systemStatusCode: '10011', + description: '请求头部 Content-Type 字段错误', + }, + { + httpStatusCode: '400', + systemStatusCode: '10014', + description: '批量参数超出单次批量数量的最大限制', + }, + { + httpStatusCode: '400', + systemStatusCode: '10016', + description: '参数缺少内容', + }, + { + httpStatusCode: '500', + systemStatusCode: '10017', + description: '参数类型错误', + }, +]; + +const CodePage: FC = () => + ({...item, key: index})) || []} + pagination={false} + />; + +export default CodePage; \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/CopyAddrList.tsx b/frontend/packages/common/src/components/aoplatform/CopyAddrList.tsx new file mode 100644 index 00000000..910b6534 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/CopyAddrList.tsx @@ -0,0 +1,86 @@ + +import { useState,FC } from 'react'; +import { Tooltip, Button } from 'antd'; +import useCopyToClipboard from '@common/hooks/copy'; +import { Icon } from '@iconify/react/dist/iconify.js'; + +type AddressItem = { + expand?: boolean; + [key: string]: unknown; +} + +type CopyAddrListProps = { + addrItem: AddressItem; + onAddrItemChange?: (addrItem: AddressItem) => void; + keyName: string; +} + +const CopyAddrList: FC = ({ addrItem, onAddrItemChange, keyName }) => { + const [localAddrItem, setLocalAddrItem] = useState(addrItem); + const { copyToClipboard } = useCopyToClipboard(); + + const toggleExpand = () => { + const updatedAddrItem = { ...localAddrItem, expand: !localAddrItem.expand }; + setLocalAddrItem(updatedAddrItem); + onAddrItemChange?.(updatedAddrItem); + }; + + const renderTooltipTitle = () => { + // 假设keyName对应的值是一个字符串数组 + const addresses:string[] = localAddrItem[keyName] as string[] + return ( +
+ {addresses?.map((addr, index) => ( +
+ {addr} +
+ ))} +
+ ); + }; + + + const renderAddresses = () => { + if (!localAddrItem.expand) { + return ( + + + + 1) ? 'w-5/6' : 'w-full'}`}> + {(localAddrItem[keyName] as string[]).join(',')} + + {(localAddrItem[keyName] as string[]).length === 1 && ( + + } + { showLastStep && } + { extraBtn } + + + } + onClose={onClose} + open={open} + > + {children} + + ) +} \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/DynamicKeyValueInput.tsx b/frontend/packages/common/src/components/aoplatform/DynamicKeyValueInput.tsx new file mode 100644 index 00000000..e822a35d --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/DynamicKeyValueInput.tsx @@ -0,0 +1,91 @@ + +import {FC } from 'react'; +import { Input, Space } from 'antd'; +import { Icon } from '@iconify/react/dist/iconify.js'; + +type KeyValueInput = { + key: string; + value: string; +}; + +type DynamicKeyValueInputProps = { + value?: KeyValueInput[]; + onChange?: (newValue: KeyValueInput[]) => void; +}; + + +export function transferToList (rawData:unknown):Array<{key:string, value:string}> { + const res:Array<{key:string, value:string}> = [] + if(!rawData) + return res + const keys:Array = Object.keys(rawData) + if (keys?.length > 0) { + for (const key of keys) { + res.push({ key: key, value: rawData[key] }) + } + return [...res, { key: '', value: '' }] +} +return [{ key: '', value: '' }] +} + +export function transferToMap (rawData:Array<{key:string, value:string}>):{[key:string]:string} { + const res:{[key:string]:string} = {} + for (const kv of rawData) { + if (kv.key && kv.value) { res[kv.key] = kv.value } + } + return res +} + +export const DynamicKeyValueInput: FC = ({value = [{key:'',value:''}],onChange}) => { + // const [keyValuePairs, setKeyValuePairs] = useState([{ key: '', value: '' }]); + +// Define a handler for when the inputs change + const handleInputChange = (index: number, type: 'key' | 'value', newValue: string) => { + // Create a new array with the updated value + const newKeyValuePairs = value ? [...value] : []; + if (newKeyValuePairs[index]) { + newKeyValuePairs[index][type] = newValue; + // If we're changing the last input and it's not empty, add a new pair + if (index === newKeyValuePairs.length - 1 && (newKeyValuePairs[index].key || newKeyValuePairs[index].value)) { + newKeyValuePairs.push({ key: '', value: '' }); + } + // Call the onChange handler if it exists + onChange?.(newKeyValuePairs); + } + }; + + const addNewPair = () => { + const newKeyValuePairs = value ? [...value, { key: '', value: '' }] : [{ key: '', value: '' }]; + onChange?.(newKeyValuePairs); + }; + + const removePair = (index: number) => { + const newKeyValuePairs = value?.filter((_, idx) => idx !== index) || []; + onChange?.(newKeyValuePairs); + }; + + return ( + <> + {value && value?.map((pair, index) => ( + + handleInputChange(index, 'key', e.target.value)} + style={{ width: 162 }} /> + handleInputChange(index, 'value', e.target.value)} + style={{ width: 162 }} /> + {index !== value.length - 1 && ( + <> + removePair(index)} width="14" height="14"/> + + + )} + + ))} + + ); +}; diff --git a/frontend/packages/common/src/components/aoplatform/EditableTable.tsx b/frontend/packages/common/src/components/aoplatform/EditableTable.tsx new file mode 100644 index 00000000..d951cedc --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/EditableTable.tsx @@ -0,0 +1,121 @@ +import { EditableProTable, ProColumns } from "@ant-design/pro-components"; +import { Button } from "antd"; +import { useState, useEffect } from "react"; +import { v4 as uuidv4} from 'uuid'; +import WithPermission from "./WithPermission"; + +interface EditableTableProps { + configFields: ProColumns[]; + value?: T[]; // 外部传入的值 + className?: string; + onChange?: (newConfigItems: T[]) => void; // 当配置项变化时,外部传入的回调函数 + // tableProps?: TableProps; + disabled?:boolean + extendsId?:string[] // 自增一行时,需要和上一行数据一致的字段,比如集群id +} + +const EditableTable = ({ + configFields, + value, // value 现在是外部传入的配置项数组 + onChange, // onChange 现在是当配置项数组变化时的回调函数 + // tableProps, + disabled, + className, + extendsId, + }: EditableTableProps) => { + // const [form] = Form.useForm(); + // const [isModalVisible, setIsModalVisible] = useState(false); + const [configurations, setConfigurations] = useState<(T | {_id:string})[]>(value ||[{_id:'1234'}]); + // const [editingConfig, setEditingConfig] = useState(null); + + const [editableKeys, setEditableRowKeys] = useState(() => + value?.map((item) => item._id) || ['1234'] + ); + + useEffect(() => { + setConfigurations(value?.map((x)=>x._id ? x : {...x,_id:uuidv4()}) || [{_id:uuidv4()}]); + }, [value]); + + const getNotEmptyValue = (value:unknown)=>{ + return value + } + + return ( + + className={className} + columns={configFields} + rowKey="_id" + value={configurations as T[]} + size="small" + bordered={true} + recordCreatorProps={false} + editable={ { + type: 'multiple', + editableKeys:disabled ? [] : configurations?.map(x=>x._id), + actionRender: (row, config) => { + return [ + , + (config.index !== configurations.length - 1 )&& , + ]; + }, + onValuesChange: (record, recordList) => { + if(record._id === recordList[recordList.length - 1]._id){ + const newId = uuidv4() + const lastRecord:{[k:string]:unknown} = recordList[recordList.length - 1]; + const newRecord :{[k:string]:unknown, _id:string}= { _id: newId }; + + // 当extendsId的长度大于0时,根据extendsId指定的字段从最后一个record中复制值 + if(extendsId && extendsId.length > 0) { + extendsId.forEach(field => { + newRecord[field] = lastRecord[field]; + }); + } + + recordList = ([...recordList, newRecord as T]); + setEditableRowKeys((prev)=>[...prev, newId]) + } + setConfigurations(recordList); + onChange?.(recordList); + }, + onChange: setEditableRowKeys, + }} + /> + ) + } + +export default EditableTable; \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/EditableTableWithModal.tsx b/frontend/packages/common/src/components/aoplatform/EditableTableWithModal.tsx new file mode 100644 index 00000000..734d2eb3 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/EditableTableWithModal.tsx @@ -0,0 +1,148 @@ +import {useEffect, useState} from 'react'; +import { Button, Modal, Form, Table, FormInstance, TableProps, Divider } from 'antd'; +import { v4 as uuidv4 } from 'uuid'; +import { ColumnsType } from 'antd/es/table'; +import WithPermission from './WithPermission'; + +export interface ConfigField { + title: string; + key: keyof T; + component: React.ReactNode; + renderText?: (value: unknown, record: T) => React.ReactNode; + required?: boolean; + ellipsis?:boolean +} + +interface EditableTableWithModalProps { + configFields: ConfigField[]; + value?: T[]; // 外部传入的值 + className?: string; + onChange?: (newConfigItems: T[]) => void; // 当配置项变化时,外部传入的回调函数 + tableProps?: TableProps; + disabled?:boolean +} + +const EditableTableWithModal = ({ + configFields, + value, // value 现在是外部传入的配置项数组 + onChange, // onChange 现在是当配置项数组变化时的回调函数 + tableProps, + disabled, + className + }: EditableTableWithModalProps) => { + const [form] = Form.useForm(); + const [isModalVisible, setIsModalVisible] = useState(false); + const [configurations, setConfigurations] = useState(value ||[]); + const [editingConfig, setEditingConfig] = useState(null); + + const showModal = (config?: T) => { + if (config) { + form.setFieldsValue(config as Record); + setEditingConfig(config); + } else { + form.resetFields(); + setEditingConfig(null); + } + setIsModalVisible(true); + }; + + const handleCancel = () => { + setIsModalVisible(false); + }; + + const handleDelete = (_id: string) => { + const newConfigurations = configurations.filter(config => config._id !== _id); + setConfigurations(newConfigurations); + onChange?.(newConfigurations); + }; + + const handleOk = () => { + form.validateFields() + .then(values => { + let newConfigurations = [...configurations]; + if (editingConfig && editingConfig._id) { + newConfigurations = newConfigurations?.map(config => + config._id === editingConfig._id ? { ...config, ...values } : config + ); + } else { + const newConfig = { _id: uuidv4(), ...values } as Record; + newConfigurations.push(newConfig as T); + } + setConfigurations(newConfigurations); + onChange?.(newConfigurations); + setIsModalVisible(false); + }) + .catch(info => { + console.log('Validate Failed:', info); + }); + }; + + useEffect(() => { + setConfigurations(value?.map((x)=>x._id ? x : {...x,_id:uuidv4()}) || []); + }, [value]); + + const columns: ColumnsType = configFields.map(({ title, key, renderText }) => ({ + title, + dataIndex: key as string, + key: key as string, + render: renderText ? (value, record) => renderText(value, record) : undefined, + ellipsis:true + })); + + !disabled && columns.push({ + title: '操作', + key: 'action', + width:117, + render: (_: unknown, record: T) => ( + <> +
+ + + +
+ + ), + }); + + const formItems = configFields.map(({ title,key, component, required }) => { + return ( + + {component} + + ) + } + ); + + return ( + <> + {!disabled && } + {configurations.length > 0 && +
} + +
+ {formItems} +
+
+ + ); +}; + +export default EditableTableWithModal; diff --git a/frontend/packages/common/src/components/aoplatform/ErrorBoundary.tsx b/frontend/packages/common/src/components/aoplatform/ErrorBoundary.tsx new file mode 100644 index 00000000..ee998b34 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/ErrorBoundary.tsx @@ -0,0 +1,23 @@ +import { useState, useEffect } from "react"; +function ErrorBoundary({ children }) { + const [error, setError] = useState(null); + + useEffect(() => { + window.addEventListener("error", (event) => { + setError(event.error); + }); + }, []); + + if (error) { + return ( +
+

An error occurred

+
{error.message}
+
+ ); + } + + return children; + } + + export default ErrorBoundary \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/GroupTree.tsx b/frontend/packages/common/src/components/aoplatform/GroupTree.tsx new file mode 100644 index 00000000..efcd2c5f --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/GroupTree.tsx @@ -0,0 +1,105 @@ + +import DirectoryTree from "antd/es/tree/DirectoryTree"; +import { DataNode, DirectoryTreeProps } from "antd/lib/tree"; +import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; +import TreeWithMore from "@common/components/aoplatform/TreeWithMore"; +import { SearchOutlined } from "@ant-design/icons"; +import { Input, Button, MenuProps } from "antd"; +import { debounce } from "lodash-es"; +import WithPermission from "@common/components/aoplatform/WithPermission"; +import { v4 as uuidv4 } from 'uuid' + +type T = unknown + +export interface GroupTreeProps extends DirectoryTreeProps{ + groupData?:(DataNode & T )[] + addBtnName?:React.ReactNode + addBtnAccess?:string + treeNameSuffixKey?:string + dropdownMenu?:(data:(DataNode & T )) => MenuProps['items'] + withMore?:boolean + onEditGroup:(type:'rename'|'addChild'|'addPeer', entity:DataNode & T, val:string) => Promise|undefined + placeholder?:string +} + +export interface GroupTreeHandle { + startEdit:(id:string)=>void; + startAdd:(type:'peer',entity?:DataNode & T)=>void +} + +const GroupTree = forwardRef((props, ref)=>{ + const {groupData,selectedKeys,onSelect,addBtnName,addBtnAccess,treeNameSuffixKey,dropdownMenu,onEditGroup,placeholder="输入以搜索"} = props + const [treeData, setTreeData] = useState([]) + const [searchWord, setSearchWord] = useState('') + const [editingId, setEditingId] = useState('') + const [addStatus, setAddStatus] = useState(false) + + useImperativeHandle(ref, ()=>({ + startEdit:setEditingId, + startAdd:handlerAction + })) + + const handlerAction = (type:'peer')=>{ + if(type === 'peer'){ + setAddStatus(true) + setEditingId(uuidv4()) + } + } + + const getTreeData = (rawData?:DataNode[])=>{ + const loop = (data: DataNode[]): DataNode[] =>{ + const newData = [...data,...(addStatus? [{title:'',key:editingId,id:editingId}]:[])] + return newData.map((item) => { + const strTitle = item.title as string; + const index = strTitle.indexOf(searchWord); + const beforeStr = strTitle.substring(0, index); + const afterStr = strTitle.slice(index + searchWord.length); + const title = + index > -1 ? ( + + {beforeStr} + {searchWord} + {afterStr} {treeNameSuffixKey && ({item?.[treeNameSuffixKey as keyof DataNode] as string ?? 0})} + ) : ( + {strTitle}{treeNameSuffixKey && ({item?.[treeNameSuffixKey as keyof DataNode] as string?? 0})} + ) + return { + title:{setAddStatus(false);setEditingId('')}} editable editingId={editingId} entity={item} afterEdit={(val)=>onEditGroup?.(addStatus && editingId === item.key ? 'addPeer':'rename',item, val)?.then((res)=>{res && setEditingId('') ;res && setAddStatus(false) ; return res})}>{title}, + key: item.key, + id:item.key + }; + }) + }; + return rawData ? loop(rawData) :[]; + } + + + const onSearchWordChange = (e:string)=>{ + setSearchWord(e || '') + } + + useEffect(()=>{ + const n = getTreeData(groupData) + setTreeData(n) + },[groupData,editingId,searchWord]) + + return ( + <> + debounce(onSearchWordChange, 100)(e.target.value)} + allowClear placeholder={placeholder} + prefix={ }/> +
+ } + blockNode={true} + treeData={treeData} + selectedKeys={selectedKeys} + onSelect={onSelect} + /> +
+ {addBtnName && } + + ) +}) + +export default GroupTree \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/InsidePage.tsx b/frontend/packages/common/src/components/aoplatform/InsidePage.tsx new file mode 100644 index 00000000..94502fa5 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/InsidePage.tsx @@ -0,0 +1,55 @@ + +import { Button, Tag } from "antd" +import {useNavigate} from "react-router-dom"; +import WithPermission from "@common/components/aoplatform/WithPermission"; +import { FC, ReactNode } from "react"; +import { ArrowLeftOutlined, LeftOutlined } from "@ant-design/icons"; + + +class InsidePageProps { + showBanner?:boolean = true + pageTitle:string = '' + tagList?:Array<{label:string|ReactNode}> = [] + children:React.ReactNode + showBtn?:boolean = false + btnTitle?:string = '' + description?:string = '' + onBtnClick?:()=>void + backUrl?:string = '/' + btnAccess?:string +} + +const InsidePage:FC = ({showBanner=true,pageTitle,tagList,showBtn,btnTitle,btnAccess,description,children,onBtnClick,backUrl})=>{ + const navigate = useNavigate(); + + const goBack = () => { + navigate(backUrl || '/'); + }; + return ( + //
+
+ { showBanner &&
+ {backUrl &&
+ +
} +
+
+

{pageTitle}

+ {tagList && tagList?.length > 0 && tagList?.map((tag)=>{ + return ( {tag.label}) + })} +
+ {showBtn && } +
+

+ {description} +

+
} +
{children}
+
+ ) +} + +export default InsidePage \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/InsidePageForHub.tsx b/frontend/packages/common/src/components/aoplatform/InsidePageForHub.tsx new file mode 100644 index 00000000..833c717a --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/InsidePageForHub.tsx @@ -0,0 +1,54 @@ + +import { Button, Tag } from "antd" +import {useNavigate} from "react-router-dom"; +import WithPermission from "@common/components/aoplatform/WithPermission"; +import { FC, ReactNode } from "react"; +import { ArrowLeftOutlined } from "@ant-design/icons"; + + +class InsidePageProps { + showBanner?:boolean = true + pageTitle:string = '' + tagList?:Array<{label:string|ReactNode}> = [] + children:React.ReactNode + showBtn?:boolean = false + btnTitle?:string = '' + description?:string = '' + onBtnClick?:()=>void + backUrl:string = '/' + btnAccess?:string +} + +const InsidePageForHub:FC = ({showBanner=true,pageTitle,tagList,showBtn,btnTitle,btnAccess,description,children,onBtnClick,backUrl})=>{ + const navigate = useNavigate(); + + const goBack = () => { + navigate(backUrl); + }; + return ( +
+ { showBanner &&
+
+ +
+
+
+ {pageTitle} + {tagList && tagList?.length > 0 && tagList?.map((tag)=>{ + return ( {tag.label}) + })} +
+ {showBtn && } +
+

+ {description} +

+
} +
{children}
+
+ ) +} + +export default InsidePageForHub \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/MemberTransfer.tsx b/frontend/packages/common/src/components/aoplatform/MemberTransfer.tsx new file mode 100644 index 00000000..ee7e81ef --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/MemberTransfer.tsx @@ -0,0 +1,250 @@ + +import { GetProp, TransferProps, TreeDataNode, theme, Transfer, Tree, Spin } from "antd"; +import { DataNode, TreeProps } from "antd/es/tree"; +import { Ref, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"; +import { TransferTableHandle, TransferTableProps } from "./TransferTable"; +import { ApartmentOutlined, LoadingOutlined, UserOutlined } from "@ant-design/icons"; +import { debounce } from "lodash-es"; + +type TransferItem = GetProp[number]; + +interface TreeTransferProps { + dataSource: TreeDataNode[]; + targetKeys: TransferProps['targetKeys']; + onChange: TransferProps['onChange']; +} + +// Customize Table Transfer +const isChecked = (selectedKeys: React.Key[], eventKey: React.Key) => + selectedKeys.includes(eventKey); + +const generateTree = ( + treeNodes: TreeDataNode[] = [], + checkedKeys: TreeTransferProps['targetKeys'] = [], + filterUnchecked: boolean = false, + disabledData:string[], + filteredItems?:Set +): TreeDataNode[] => { + const checkedKeysSet = new Set(checkedKeys); + return treeNodes + .map(({ children, ...props }) => { + const childNodes = generateTree(children, checkedKeys, filterUnchecked, disabledData, filteredItems); + const isDisabled = (!filterUnchecked && disabledData && disabledData.indexOf(props.id as string) !== -1) + ? true + : (filterUnchecked ? false : checkedKeysSet.has(props.id as string)); + const hasEnabledChild = childNodes.some(node => !node.disabled); + + return { + ...props, + title: {props.name}, + key: props.id, + disabled: isDisabled && !hasEnabledChild, + children: childNodes, + }; + }) + .filter(node => { + let res:boolean= true + if(filterUnchecked){ + res =(!disabledData || disabledData.indexOf(node.key as string) === -1) && (checkedKeysSet.has(node.key as string) || (node.children && node.children.length > 0) ) + } + + if(filterUnchecked && filteredItems &&((filteredItems.size && !filteredItems.has(node.key as string))&& !(node.children && node.children.length > 0) )){ + return false + } + return res + } + ) +}; + +const TransferTree = (props)=>{ + const { direction, token, tableHeight, dataSource, targetKeys, onItemSelect, onItemSelectAll,checkedKey,selectedKeys, filteredItems ,disabledData} = props; + const [expandedKeys, setExpandedKeys] = useState([]); + + const getExpandedKeys = (newData:TreeDataNode[], expandedSet:Set = new Set())=>{ + newData.forEach((item)=>{ + if(item.children && item.children.length > 0){ + expandedSet.add(item.key) + getExpandedKeys(item.children,expandedSet) + } + }) + return expandedSet + } + + const treeData:TreeDataNode[] = useMemo(()=>{ + const filteredSet = filteredItems && filteredItems.length > 0 ? new Set(filteredItems.map((x)=>x.id)) : new Set() + const res = dataSource && dataSource.length > 0 ? generateTree(dataSource, targetKeys,direction === 'right',disabledData,filteredSet) : [] + setExpandedKeys(Array.from(getExpandedKeys(res))) + return res + },[ + dataSource, targetKeys,direction ,disabledData,filteredItems + ]) + + const onExpand: TreeProps['onExpand'] = (expandedKeysValue) => { + setExpandedKeys(expandedKeysValue as string[]); + }; + + return ( + +
+ { return (props.type === 'member' ? : )} } + treeData={treeData} + onCheck={(_checkedKeys, e:{checked: boolean, checkedNodes, node, event, halfCheckedKeys}) => { + if(e.checked){ + onItemSelectAll( _checkedKeys, e.checked); + }else{ + const checkedKeyArrFromTree = e.checkedNodes.map(node => node.key) + onItemSelectAll((checkedKey as string[]).filter(key => checkedKeyArrFromTree.indexOf(key) === -1),e.checked) + } + }} + onSelect={(_, { node: { key } }) => { + onItemSelect(key as string, !isChecked(checkedKey, key)); + }} + /> +
+ ) +} + + + const MemberTransfer= forwardRef, TransferTableProps<{[k:string]:unknown}>>( + (props: TransferTableProps, ref:Ref>) => { + const {request,columns,primaryKey,onSelect,tableType,disabledData = [],searchPlaceholder} = props + const [tableHeight, setTableHeight] = useState(window.innerHeight * 80 / 100 - 64 - 72 - 56 - 16 -3); + const [targetKeys, setTargetKeys] = useState([]); + const [dataSource, setDataSource] = useState([]) + const parentRef = useRef(null); + const [loading, setLoading] = useState(false) + + + useEffect(()=>{ + setTargetKeys(disabledData) + },[disabledData]) + + useImperativeHandle(ref, () =>({ + selectedData: () => dataSource, + selectedRowKeys: () => targetKeys,})) + + const onChange: TreeTransferProps['onChange'] = (keys) => { + onSelect?.(new Set(keys)) + setTargetKeys(Array.from(new Set(keys))); + }; + + const { token } = theme.useToken(); + + const transferDataSource: TransferItem[] = useMemo(()=>{ + function flatten(list: TreeDataNode[] = [], res:TransferItem[]) { + list.forEach((item) => { + res.push(item as TransferItem); + flatten(item.children,res); + }); + } + const res:TransferItem[] =[] + flatten(dataSource,res); + return res + },[ + dataSource + ]) + + let memo: Record = {}; + + const handlerFilterOption = (inputValue: string, item: any, parentResult: boolean = false, childrenSet: Set = new Set()): boolean => { + const cacheKey = `${inputValue}_${item.key}`; + if (memo[cacheKey]) { + return memo[cacheKey]; + } + + childrenSet.add(item.key); + let result = item.title.includes(inputValue) || parentResult + if (item.children) { + for (const child of item.children) { + if (handlerFilterOption(inputValue, child, result,childrenSet)) { + result = true; + } + } + } + + if (result) { + memo[cacheKey] = result; + childrenSet.forEach((key) => { + memo[`${inputValue}_${key}`] = result; + }); + } + return result; + }; + + const getDataSource = ()=>{ + setLoading(true) + request && request().then((res)=>{ + const {data,success} = res + setDataSource(success? data : []) + }).finally(()=>{setLoading(false)}) +} + +useEffect(() => { + getDataSource() + const handleResize = () => { + setTableHeight(window.innerHeight * 80 / 100 - 64 - 72 - 56 - 16 -3) + }; + + const debouncedHandleResize = debounce(handleResize, 200); + + // 监听窗口大小变化 + window.addEventListener('resize', debouncedHandleResize); + handleResize(); + return () => { + window.removeEventListener('resize', debouncedHandleResize); + }; +}, []); + + return ( +
+ } spinning={loading} className=''> + { + memo = {}; + }} + listStyle={{width:'408px'}} + disabledData={disabledData} + filterOption={(inputValue: string, item: any) => handlerFilterOption(inputValue, item)} + targetKeys={targetKeys} + dataSource={transferDataSource} + className="tree-transfer" + render={(item) => item.title!} + showSelectAll={false} + onChange={onChange} + titles={['','']} + > + {({ direction, onItemSelect, selectedKeys,onItemSelectAll ,filteredItems}) => { + const treeProps = { + dataSource, direction, onItemSelect, selectedKeys,onItemSelectAll ,filteredItems,token,tableHeight,targetKeys,disabledData + } + if (direction === 'left') { + const checkedKey = [...selectedKeys, ...targetKeys as string[]]; + return ( + + ); + } + if(direction === 'right'){ + const checkedKey = [...selectedKeys,...targetKeys as string[]]; + return ( + + ); + } + }} + + +
+ ); +}) + +export default MemberTransfer; \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/MonacoEditorWrapper.tsx b/frontend/packages/common/src/components/aoplatform/MonacoEditorWrapper.tsx new file mode 100644 index 00000000..911e47cc --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/MonacoEditorWrapper.tsx @@ -0,0 +1,20 @@ +import { useEffect } from "react"; +import { editor } from "monaco-editor"; +import useInitializeMonaco from "@common/hooks/useInitializeMonaco"; +import { Editor, useMonaco } from '@monaco-editor/react' + +export type MonacoEditorRefType = editor.IStandaloneCodeEditor; +const MonacoEditorWrapper: React.FC = (props) => { + useInitializeMonaco(); + const monacoInstance = useMonaco(); + + useEffect(() => { + if (monacoInstance) { + // 在这里你可以访问并配置Monaco实例 + } + }, [monacoInstance]); + + return ; +}; + +export default MonacoEditorWrapper; \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/Navigation.tsx b/frontend/packages/common/src/components/aoplatform/Navigation.tsx new file mode 100644 index 00000000..afd18c5c --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/Navigation.tsx @@ -0,0 +1,100 @@ + +import {FC, useEffect, useMemo, useState} from 'react'; +import type { MenuProps } from 'antd'; +import { Menu } from 'antd'; +import { useLocation, useNavigate} from "react-router-dom"; +import { getNavItem } from '@common/utils/navigation'; +import { PERMISSION_DEFINITION } from '@common/const/permissions'; +import { useGlobalContext } from '@common/contexts/GlobalStateContext'; +import { ProjectFilled } from '@ant-design/icons'; +import { Icon } from '@iconify/react'; +export type MenuItem = Required['items'][number]; + +const APP_MODE = import.meta.env.VITE_APP_MODE; + +// avoid changing route within ths same category +export const routerKeyMap = new Map([ + ['workspace',['tenantManagement','service','team','serviceHub']], + ['my',['tenantManagement','service','team']], + ['mainPage',['dashboard','systemrunning']], + ['operationCenter',['member','user','role','servicecategories']], + ['organization',['member','user','role']], + ['serviceHubSetting',['servicecategories']], + ['maintenanceCenter',['partition','logsettings','resourcesettings','openapi'] +]]) + + +export const TOTAL_MENU_ITEMS: MenuProps['items'] = [ + + getNavItem('工作空间', 'workspace','/tenantManagement',, [ + getNavItem('我的', 'my','/tenantManagement',null,[ + getNavItem(应用, 'tenantManagement','/tenantManagement',,undefined,undefined,''), + getNavItem(服务, 'service','/service',,undefined,undefined,''), + getNavItem(团队, 'team','/team',,undefined,undefined,''), + ],undefined,''), + getNavItem(API 市场, 'serviceHub','/serviceHub',,undefined,undefined,'system.workspace.api_market.view'), + ]), + + + APP_MODE === 'pro' ? getNavItem('仪表盘', 'mainPage', '/dashboard',,[ + getNavItem(运行视图, 'dashboard','/dashboard',,undefined,undefined,''), + getNavItem(系统拓扑图, 'systemrunning','/systemrunning',,undefined,undefined,''), + ]):null, + + getNavItem('系统设置', 'operationCenter','/member',, [ + getNavItem('组织', 'organization','/member',null,[ + getNavItem(成员, 'member','/member',,undefined,undefined,'system.organization.member.view'), + getNavItem(角色, 'role','/role',,undefined,undefined,'system.organization.role.view'), + ],undefined,''), + getNavItem('API 市场', 'serviceHubSetting','/servicecategories',null,[ + getNavItem(服务分类管理, 'servicecategories','/servicecategories',,undefined,undefined,'system.api_market.service_classification.view'), + ],undefined,'system.api_market.service_classification.view'), + + getNavItem('运维与集成', 'maintenanceCenter','/cluster', null, [ + getNavItem(集群, 'cluster','/cluster',,undefined,undefined,'system.devops.cluster.view'), + getNavItem(证书, 'cert','/cert',,undefined,undefined,'system.devops.ssl_certificate.view'), + getNavItem(日志, 'logsettings','/logsettings',,undefined,undefined,'system.devops.log_configuration.view'), + APP_MODE === 'pro' ? getNavItem(资源, 'resourcesettings','/resourcesettings',null,undefined,undefined,'system.partition.self.view'):null, + APP_MODE === 'pro' ? getNavItem(Open API, 'openapi','/openapi',null,undefined,undefined,'system.openapi.self.view'):null, + ]), + ]), +]; + +const Navigation: FC = () => { + const location = useLocation() + const [selectedKeys, setSelectedKeys] = useState('') + const currentUrl = location.pathname + const navigateTo = useNavigate() + const { accessData,checkPermission} = useGlobalContext() + + const onClick: MenuProps['onClick'] = (e) => { + if(location.pathname.split('/')[1] === e.key) return + const newUrl = routerKeyMap.get(e.key) + newUrl && navigateTo(newUrl) + }; + + const menuData = useMemo(()=>{ + const filterMenu = (menu:Array<{[k:string]:unknown}>)=>{ + return menu.filter(x=> x && (x.access ? checkPermission(x.access as keyof typeof PERMISSION_DEFINITION[0]): true)) + } + return TOTAL_MENU_ITEMS!.filter(x=>x).map((x)=> ( x.children ? {...x, children:filterMenu(x.children)} : x))?.filter(x=> x.key === 'service' || (x.children && x.children?.length > 0)) +},[accessData]) + + useEffect(() => { + setSelectedKeys(currentUrl.split('/')[1] === 'template' ? currentUrl.split('/')[2] : currentUrl.split('/')[1]) + }, [currentUrl]); + + return ( + + ); +}; + +export default Navigation; \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/PageList.module.css b/frontend/packages/common/src/components/aoplatform/PageList.module.css new file mode 100644 index 00000000..a5b0918f --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/PageList.module.css @@ -0,0 +1,48 @@ + +:global .eo_page_list .ant-pro-card .ant-pro-card-body{ + padding:0 !important; +} + +:global .eo_page_list .ant-pro-table-list-toolbar-container{ + padding:10px 20px 10px 10px !important; + + .ant-pro-table-list-toolbar-right{ + justify-content: flex-start; + flex-direction: row-reverse; + } + + .ant-input-group-addon .ant-input-search-button{ + display:none; + } +} +:global .eo_page_list .ant-table-wrapper { + .ant-table-pagination.ant-pagination { + margin: 1px 10px 0 !important; + padding: 10px 0; + /* box-shadow: 0 -2px 2px -2px var(--border-color); */ + } + .ant-table.ant-table-middle{ + .ant-table-thead>tr>th, + .ant-table-thead>tr>td{ + border-top:1px solid var(--border-color); + border-bottom:1px solid var(--border-color); + } + .ant-table-footer,.ant-table-cell, + .ant-table-thead>tr>th, + .ant-table-tbody>tr>th{ + padding:8px 10px; + } + + + .ant-table-thead>tr>th{ + color:#666666; + font-weight:normal; + } + } + + .ant-table.ant-table-middle tfoot>tr>th, + .ant-table.ant-table-middle tfoot>tr>td{ + padding:8px 10px; + } +} + diff --git a/frontend/packages/common/src/components/aoplatform/PageList.tsx b/frontend/packages/common/src/components/aoplatform/PageList.tsx new file mode 100644 index 00000000..bf06ea5d --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/PageList.tsx @@ -0,0 +1,232 @@ + +import {Button, Dropdown, Input, MenuProps, TablePaginationConfig} from 'antd'; +import {ChangeEvent, RefAttributes, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import type {ActionType, ParamsType, ProColumns, ProTableProps} from '@ant-design/pro-components'; +import { + DragSortTable, + ProTable, +} from '@ant-design/pro-components'; +import './PageList.module.css' +import {SearchOutlined} from "@ant-design/icons"; +import { debounce } from 'lodash-es' +import WithPermission from '@common/components/aoplatform/WithPermission'; +import { FilterValue, SorterResult, TableCurrentDataSource } from 'antd/es/table/interface'; +import { useGlobalContext } from '../../contexts/GlobalStateContext'; +import { PERMISSION_DEFINITION } from '@common/const/permissions'; +import { withMinimumDelay } from '@common/utils/ux'; + +interface PageListProps extends ProTableProps, RefAttributes { + id?:string + columns: ProColumns[] + request?:(params: (ParamsType & {pageSize?: number | undefined, current?: number | undefined, keyword?: string | undefined}), sorter: unknown, filter: unknown)=>Promise<{data:T[], success:boolean}> + dropMenu?:MenuProps + searchPlaceholder?:string + showPagination?:boolean + primaryKey?:string + addNewBtnTitle?:string + addNewBtnAccess?:string + tableClickAccess?:string + onAddNewBtnClick?:()=>void + beforeSearchNode?:React.ReactNode[] + onSearchWordChange?:(e:ChangeEvent) => void + afterNewBtn?:React.ReactNode[] + dragSortKey?:string + onDragSortEnd?:(beforeIndex: number, afterIndex: number, newDataSource: T[]) => void | Promise + tableTitle?:string + dataSource?:T[] + onRowClick?:(record:T)=>void + showColSetting?:boolean + minVirtualHeight?:number + besidesTableHeight?:number + noTop?:boolean + tableClass?:string + tableTitleClass?:string + addNewBtnWrapperClass?:string + delayLoading?:boolean + noScroll?:boolean + /* 前端分页的表格,需要传入该字段以支持后端搜索 */ + manualReloadTable?:()=>void +} + + +const PageList = >(props: React.PropsWithChildren>,ref: React.Ref) => { + const {id,columns,request,dropMenu,searchPlaceholder,showPagination=true,primaryKey='id',addNewBtnTitle,addNewBtnAccess,tableClickAccess,tableClass,onAddNewBtnClick,beforeSearchNode,onSearchWordChange,manualReloadTable,afterNewBtn,dragSortKey,onDragSortEnd,tableTitle,rowSelection,onChange,dataSource,onRowClick,showColSetting=false,minVirtualHeight,noTop,addNewBtnWrapperClass,tableTitleClass,delayLoading = true,besidesTableHeight, noScroll} = props + const parentRef = useRef(null); + const [tableHeight, setTableHeight] = useState(minVirtualHeight || window.innerHeight); + const [tableWidth, setTableWidth] = useState(undefined); + const actionRef = useRef(); + const [allowTableClick,setAllowTableClick] = useState(false) + const {accessData,checkPermission} = useGlobalContext() + const [minTableWidth, setMinTableWidth] = useState(0) + + // 使用useImperativeHandle来自定义暴露给父组件的实例值 + useImperativeHandle(ref, () => actionRef.current!); + + const lastAccess = useMemo(()=>{ + if(!tableClickAccess) return true + return checkPermission(tableClickAccess as keyof typeof PERMISSION_DEFINITION[0]) +},[allowTableClick, accessData]) + + useEffect(()=>{ + tableClickAccess ? setAllowTableClick(lastAccess) : setAllowTableClick(true) + },[accessData]) + + const resizeObserverRef = useRef(null); + + useEffect(() => { + const handleResize = () => { + if (parentRef.current && !noScroll) { + const res = parentRef.current.getBoundingClientRect(); + const height = res.height - ((noTop ? 0 : 52) + 40 + (showPagination && !dragSortKey ? 52 : 0) +( besidesTableHeight ?? 0)); // 减去顶部按钮、底部分页、表头高度 + setTableWidth(minTableWidth > res.width ? minTableWidth : undefined); + height && setTableHeight(minVirtualHeight === undefined ? height : (height > minVirtualHeight ? height : minVirtualHeight)); + } + }; + + const debouncedHandleResize = debounce(handleResize, 200); + + if (!resizeObserverRef.current && !noScroll) { + // 创建一个 ResizeObserver 来监听高度变化,只创建一次 + resizeObserverRef.current = new ResizeObserver(debouncedHandleResize); + // 开始监听 + if (parentRef.current && !minVirtualHeight) { + resizeObserverRef.current.observe(parentRef.current); + } + } + + // 在 minTableWidth 变化时手动触发 handleResize + handleResize(); + + // 清理函数 + return () => { + if (resizeObserverRef.current) { + resizeObserverRef.current.disconnect(); + resizeObserverRef.current = null; + } + }; + }, [minTableWidth, parentRef, noTop, showPagination, dragSortKey, minVirtualHeight]); // 将相关依赖项作为 useEffect 的依赖项 + + + + + const newColumns = useMemo(()=>{ + let width:number = 0 + const res = columns?.map( + (x)=>{ + width += Number(x.width ?? ((x.filters || x.sorter) ? 120 : 100)) + x.copyable = x.copyable === false? false: true + const sorter = localStorage.getItem(`${id}_sorter`) + const filters = localStorage.getItem(`${id}_filters`) + if(sorter && x.sorter){ + const sorterObj = JSON.parse(sorter) + const xName = Array.isArray(x.dataIndex) ? x.dataIndex.join(','):x.dataIndex + x.defaultSortOrder = sorterObj?.columnKey === xName ? sorterObj?.order : undefined + // x.showSorterTooltip = {target:'sorter-icon'} + } + if(filters && x.filters){ + const filtersObj = JSON.parse(filters) + const xName = Array.isArray(x.dataIndex) ? x.dataIndex.join(','):x.dataIndex + x.defaultFilteredValue = filtersObj?.[xName as string] + } + return x}) + setMinTableWidth(width) + return res + },[columns]) + + const headerTitle = ()=>{ + return ( + <>{ + tableTitle ? {tableTitle} : ( + addNewBtnTitle ? : undefined + ) + + } + {afterNewBtn ? afterNewBtn as React.ReactNode[] :undefined} + + ) + } + + const requestWithDelay = (params: ParamsType & { pageSize?: number | undefined; current?: number | undefined; keyword?: string | undefined;}, sort: unknown, filter: unknown) => { + return withMinimumDelay(() => request!(params, sort, filter), delayLoading === false? 0 : undefined); + }; + + return ( +
+ {dragSortKey? + actionRef={actionRef} + columns={newColumns} + rowKey={primaryKey} + search={false} + pagination={false} + request={request} + dragSortKey={dragSortKey} + onDragSortEnd={onDragSortEnd} + scroll={noScroll ? undefined :{ y: tableHeight }} + options={{ + reload: false, + density: false, + setting: false, + }} + headerTitle={ + headerTitle() + } + /> : + actionRef={actionRef} + columns={newColumns} + virtual + scroll={noScroll ? undefined : {x:tableWidth,y: tableHeight }} + size="middle" + rowSelection={rowSelection} + tableAlertRender={false} + tableAlertOptionRender={false} + request={request ? requestWithDelay : undefined} + toolBarRender={() => [ + dropMenu ? ( + + ):null, + ]} + toolbar={{ + actions:[...[beforeSearchNode],...[searchPlaceholder? debounce(onSearchWordChange, 100)(e) : undefined } onPressEnter={()=>manualReloadTable ? manualReloadTable():actionRef.current?.reload?.()} allowClear placeholder={searchPlaceholder} prefix={{actionRef.current?.reload?.()}}/>}/>:null]], + }} + options={{ + reload: false, + density: false, + setting: showColSetting ? { + draggable:false, + showListItemOption:false + } :false, + }} + showSorterTooltip={false} + columnsState={{persistenceType:'localStorage',persistenceKey:id}} + pagination={showPagination ? { + showSizeChanger: true, + showQuickJumper: true, + size:'default' + }:false} + rowKey={primaryKey} + onChange={(pagination: TablePaginationConfig, filters: Record, sorter: SorterResult | SorterResult[],extra:TableCurrentDataSource) =>{ + localStorage.setItem(`${id}_filters`,JSON.stringify(filters)) + !Array.isArray(sorter) && localStorage.setItem(`${id}_sorter`,JSON.stringify({columnKey:sorter?.columnKey, order: sorter?.order})) + onChange?.(pagination,filters,sorter,extra)}} + dataSource={dataSource} + search={false} + headerTitle={ + headerTitle() + } + onRow={onRowClick && allowTableClick ? (record) => ({ + onClick: () => { + onRowClick(record); + } + }):undefined} + rowClassName={()=>onRowClick && allowTableClick ?"cursor-pointer":''} + />} +
+ ); +}; + +export default forwardRef(PageList) as >(props: React.PropsWithChildren> & { ref?: React.Ref }) => ReturnType; \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/PublishApprovalModalContent.tsx b/frontend/packages/common/src/components/aoplatform/PublishApprovalModalContent.tsx new file mode 100644 index 00000000..c8e6a8c9 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/PublishApprovalModalContent.tsx @@ -0,0 +1,296 @@ +import {App, Col, Form, Input, Row, Table, Tooltip} from "antd"; +import {forwardRef, useEffect, useImperativeHandle} from "react"; +import {PublishApprovalInfoType, PublishVersionTableListItem} from "@common/const/approval/type.tsx"; +import {useFetch} from "@common/hooks/http.ts"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import { SYSTEM_PUBLISH_ONLINE_COLUMNS } from "@core/const/system/const.tsx"; +import { SystemInsidePublishOnlineItems } from "@core/pages/system/publish/SystemInsidePublishOnline.tsx"; + +enum ChangeTypeEnum { + 'new' = '新增', + 'update' = '变更', + 'delete' = '删除', + 'none' = '无变更', + 'error' = '缺失字段' +} + +const statusColorClass = { + new: 'text-[#138913]', // 使用 Tailwind 的 Arbitrary Properties + update: 'text-[#03a9f4]', + delete: 'text-[#ff3b30]', + none: 'text-[var(--MAIN_TEXT)]', // 假设你也有一个“none”的状态 + }; + +const apiColumns = [ + { + title:'API 名称', + dataIndex:'name', + copyable: true, + ellipsis:true + }, + { + title:'请求方式', + dataIndex:'method', + copyable: true, + ellipsis:true + }, + { + title:'路径', + dataIndex:'path', + copyable: true, + ellipsis:true + }, + { + title:'类型', + dataIndex:'change', + render:(_,entity)=>( + + + {ChangeTypeEnum[entity.change as (keyof typeof ChangeTypeEnum)] || '-'} + {entity.change === 'error' ?` 该 API 缺失 ${entity.proxyStatus == 1 && '转发信息,'} ${entity.docStatus == 1 && '文档信息,'} ${entity.upstreamStatus == 1 && '上游信息,'}请先补充`:''} + + ) + + } +] + +const upstreamColumns = [ + { + title:'上游类型', + dataIndex:'type', + ellipsis:true, + // filters: true, + // onFilter: true, + // valueType: 'select', + // filterSearch: true, + valueEnum:{ + 'static':{ + text:'静态上游' + }, + // 'dynamic':{ + // text:'动态上游' + // } + } + }, + { + title:'地址', + dataIndex:'addr', + render:(text:string[])=>(<>{text.join(',')}), + copyable: true, + ellipsis:true + }, + { + title:'类型', + dataIndex:'change', + render:(_,entity)=>( + + {ChangeTypeEnum[entity.change as (keyof typeof ChangeTypeEnum)] || '-'} + {entity.change === 'error' ?` 该 API 缺失 ${entity.proxyStatus == 1 && '转发信息,'} ${entity.docStatus == 1 && '文档信息,'} ${entity.upstreamStatus == 1 && '上游信息,'}请先补充`:''} + ) + } +] + +type PublishApprovalModalProps = { + type:'approval'|'view'|'add'|'publish'|'online' + data:PublishApprovalInfoType | PublishApprovalInfoType &{id?:string} | PublishVersionTableListItem + insideSystem?:boolean + serviceId:string + teamId:string + clusterPublishStatus?:SystemInsidePublishOnlineItems[] +} + +export type PublishApprovalModalHandle = { + save:(operate:'pass'|'refuse') =>Promise + publish:(notSave?:boolean)=>Promise> + online:()=>Promise +} + +export const PublishApprovalModalContent = forwardRef((props, ref) => { + const { message } = App.useApp() + const { type,data,insideSystem = false,serviceId, teamId} = props + const [form] = Form.useForm(); + const {fetchData} = useFetch() + + const save:(operate:'pass'|'refuse')=>Promise = (operate)=>{ + if(type === 'view'){ + return Promise.resolve(true) + } + return form.validateFields().then((value)=>{ + if(operate === 'refuse' && form.getFieldValue('opinion') === '' ){ + form.setFields([{ + name:'opinion',errors:['选择拒绝时,审批意见为必填'] + }]) + form.scrollToField('opinion') + return Promise.reject('未填写审核意见') + } + return fetchData>(`service/publish/${operate === 'pass' ? 'accept' : 'refuse'}`,{method: 'PUT',eoBody:({comments:value.opinion}), eoParams:{id:data!.id, project:serviceId},eoTransformKeys:['versionRemark']}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + return Promise.resolve(true) + }else{ + message.error(msg || '操作失败') + return Promise.reject(msg || '操作失败') + } + }).catch((errorInfo)=> Promise.reject(errorInfo)) + }).catch((err)=> {form.scrollToField(err.errorFields[0].name[0]); return Promise.reject(err)}) + } + + const publish:(notSave?:boolean)=>Promise> = (notSave)=>{ + return new Promise((resolve, reject)=>{ + form.validateFields().then((value)=>{ + const body = {...value, ...(type === 'publish'&&{release:data.id})} + fetchData>( + notSave ? 'service/publish/apply' : 'service/publish/release/do',{method: 'POST',eoBody:body, eoParams:{service:serviceId, team:teamId},eoTransformKeys:['versionRemark']}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(response) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + const online:()=>Promise = ()=>{ + return new Promise((resolve, reject)=>{ + form.validateFields().then(()=>{ + fetchData>('service/publish/execute',{method: 'PUT', eoParams:{project:serviceId,id:(data as PublishVersionTableListItem).flowId},eoTransformKeys:['versionRemark']}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + useImperativeHandle(ref, ()=>({ + save, + publish, + online + }) + ) + + useEffect(()=>{ + form.setFieldsValue({ opinion:'',...data}) + },[]) + + return ( + <> + {!insideSystem && <> + +
申请系统: + {(data as PublishApprovalInfoType).project || '-'} + + + + 所属团队: + {(data as PublishApprovalInfoType).team || '-'} + + + + 申请人: + {(data as PublishApprovalInfoType).applier || '-'} + + + + 申请时间: + {(data as PublishApprovalInfoType).applyTime || '-'} + + } +
+ + { + insideSystem && + <> + + + + + + + + + } + API 列表: + +
+ 上游列表: + +
+ + + + + {type !== 'add' && type !== 'publish' && + { form.setFields([ + { + name: 'opinion', + errors: [], // 设置为空数组来移除错误信息 + }, + ]);}}/> + } + + {['error','done'].indexOf(data.status) !== -1 && data.clusterPublishStatus &&data.clusterPublishStatus.length > 0 && <> 上线情况: + +
+ } + + + ) +}) \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/ResetPsw.tsx b/frontend/packages/common/src/components/aoplatform/ResetPsw.tsx new file mode 100644 index 00000000..b45e71c6 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/ResetPsw.tsx @@ -0,0 +1,134 @@ +import { Form, Input} from "antd"; +import {forwardRef, useEffect, useImperativeHandle} from "react"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import { UserInfoType } from "@common/const/type.ts"; + +type FieldType = { + userName:string + old:string + password:string + confirm:string +} +type ResetPswProps = { + entity?:UserInfoType +} + +export type ResetPswHandle = { + save:()=>Promise +} + +export const ResetPsw = forwardRef((props,ref)=>{ + const [form] = Form.useForm(); + + const save:()=>Promise = ()=>{ + return new Promise((resolve)=>{ + // form.validateFields().then((value)=>{ + // fetchData>(url,{method,eoBody:(value), eoTransformKeys:['departmentIds']}).then(response=>{ + // const {code,msg} = response + // if(code === STATUS_CODE.SUCCESS){ + // message.success(msg || '操作成功!') + resolve(true) + // }else{ + // message.error(msg || '操作失败') + // reject(msg || '操作失败') + // } + // }) + // }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + const getPswStrength = (value: string) => { + const pswRegNum: RegExp = /[0-9]/ + const pswRegLowercase: RegExp = /[a-z]/ + const pswRegUppercase: RegExp = /[A-Z]/ + const pswRegSymbol: RegExp = /!@#$%^&*`~()-+=/ + let strength: number = 0 + if (pswRegNum.test(value)) { + strength++ + } + if (pswRegLowercase.test(value)) { + strength++ + } + if (pswRegUppercase.test(value)) { + strength++ + } + if (pswRegSymbol.test(value)) { + strength++ + } + return strength + } + + useImperativeHandle(ref, ()=>({ + save + }) + ) + + useEffect(() => { + // form.setFieldsValue({id:entity!.id}) + }, []); + + return ( +
+ + label="账号" + name="userName" + hidden + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + + + + label="旧密码" + name="old" + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + + + + label="新密码" + name="password" + hidden + // eslint-disable-next-line @typescript-eslint/no-unused-vars + rules={[{ required: true, message: '必填项',whitespace:true }, ({ getFieldValue }) => ({ + validator(_, value) { + if (!value || getPswStrength(value)>1) { + return Promise.resolve(); + } + return Promise.reject(new Error('密码强度:弱,建议使用英文、数字、特殊字符组合')); + }, + })]} + > + + + + + label="确认新密码" + name="confirm" + dependencies={['password']} + rules={[{ required: true, message: '必填项',whitespace:true }, ({ getFieldValue }) => ({ + validator(_, value) { + if (!value || getFieldValue('password') === value) { + return Promise.resolve(); + } + return Promise.reject(new Error('新密码与确认新密码不一致')); + }, + })]} + > + + + + +
) +}) \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/ScrollableSection.tsx b/frontend/packages/common/src/components/aoplatform/ScrollableSection.tsx new file mode 100644 index 00000000..32b3c944 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/ScrollableSection.tsx @@ -0,0 +1,64 @@ + +import {FC, useRef, useEffect, Children, cloneElement, isValidElement } from 'react'; + +interface ScrollableSectionProps { + children: React.ReactNode; +} + +const ScrollableSection: FC = ({ children }) => { + const scrollAreaRef = useRef(null); + + useEffect(() => { + const handleScroll = () => { + if (scrollAreaRef.current) { + const scrollTop = scrollAreaRef.current.scrollTop; + const scrollHeight = scrollAreaRef.current.scrollHeight; + const clientHeight = scrollAreaRef.current.clientHeight; + + // 如果滚动到顶部,.content-before 应该显示阴影 + const showTopShadow = scrollTop > 0; + // 如果滚动到底部,.content-after 应该显示阴影 + const showBottomShadow = scrollHeight - scrollTop < clientHeight; + // 这里我们不直接更新状态,而是通过ref来设置样式 + if (showTopShadow && !showBottomShadow) { + setElementShadow('.content-before', true); + setElementShadow('.content-after', false); + } else if (!showTopShadow && showBottomShadow) { + setElementShadow('.content-before', false); + setElementShadow('.content-after', true); + } else { + setElementShadow('.content-before', false); + setElementShadow('.content-after', false); + } + } + }; + + scrollAreaRef.current?.addEventListener('scroll', handleScroll); + + return () => { + scrollAreaRef.current?.removeEventListener('scroll', handleScroll); + }; + }, []); + + const setElementShadow = (elementSelector: string, showShadow: boolean) => { + const element = document.querySelector(elementSelector); + if (element) { + element.style.boxShadow = showShadow ? ( elementSelector === '.content-before' ? '0 2px 2px #0000000d':'0 -2px 2px -2px var(--border-color)') : 'none'; + } + } + + const childrenWithRef = Children.toArray(children).map((child) => { + if (isValidElement(child) && child.props.className && child.props.className.includes('scroll-area')) { + // 将 ref 附加到具有 'scroll-area' 类名的子元素 + return cloneElement(child, { ref: scrollAreaRef }); + } + return child; + }); + + return ( + <> {childrenWithRef} + + ); +}; + +export default ScrollableSection; \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/SubscribeApprovalModalContent.tsx b/frontend/packages/common/src/components/aoplatform/SubscribeApprovalModalContent.tsx new file mode 100644 index 00000000..9f994993 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/SubscribeApprovalModalContent.tsx @@ -0,0 +1,133 @@ +import {App, Checkbox, Col, Form, Input, Row} from "antd"; +import { forwardRef, useEffect, useImperativeHandle} from "react"; +import {SubscribeApprovalInfoType} from "@common/const/approval/type.tsx"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; + +type SubscribeApprovalModalProps = { + type:'approval'|'view' + data?:SubscribeApprovalInfoType + inSystem?:boolean + serviceId:string + teamId:string +} + +export type SubscribeApprovalModalHandle = { + save:(operate:'pass'|'refuse') =>Promise +} + +type FieldType = { + reason?:string; + opinion?:string; +}; + +const list = [ + { + title:'申请方应用',key:'application' + }, + { + title:'申请方所属团队',key:'applyTeam' + }, + { + title:'申请人',key:'applier' + }, + { + title:'申请时间',key:'applyTime' + }, + { + title:'申请服务',key:'service' + }, + { + title:'服务所属团队',key:'team' + } +] +export const SubscribeApprovalModalContent = forwardRef((props, ref) => { + const { message } = App.useApp() + const {data, type,inSystem=false, teamId, serviceId} = props + const [form] = Form.useForm(); + const {fetchData} = useFetch() + + const save:(operate:'pass'|'refuse')=>Promise = (operate)=>{ + return new Promise((resolve, reject)=>{ + if(type === 'view'){ + resolve(true) + return + } + form.validateFields().then((value)=>{ + if(operate === 'refuse' && form.getFieldValue('opinion') === ''){ + form.setFields([{ + name:'opinion',errors:['必填项'] + }]) + form.scrollToField('opinion') + reject('未填写审核意见') + return + } + fetchData>(`${inSystem?'service/':''}approval/subscribe`,{method: 'POST',eoBody:({opinion:value.opinion,operate}), eoParams:(inSystem ? {apply:data!.id, team:teamId} : {id:data!.id,team:teamId})}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + useImperativeHandle(ref, ()=>({ + save + }) + ) + + useEffect(()=>{ + form.setFieldsValue({opinion:'',...data}) + },[]) + + return ( +
{ + list?.map((x)=>( + +
{x.title}: + {(data as {[k:string]:unknown})?.[x.key]?.name || (data as {[k:string]:unknown})?.[x.key] || '-'} + + )) + } + +
+ + + label="申请原因" + name="reason" + > + + + + label="审核意见" + name="opinion" + extra="选择拒绝时,审批意见为必填" + > + { form.setFields([ + { + name: 'opinion', + errors: [], // 设置为空数组来移除错误信息 + }, + ])}} /> + + +
+ + ) +}) \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/TableBtnWithPermission.tsx b/frontend/packages/common/src/components/aoplatform/TableBtnWithPermission.tsx new file mode 100644 index 00000000..4d0223d4 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/TableBtnWithPermission.tsx @@ -0,0 +1,43 @@ + +import { Button, Tooltip } from "antd" +import { useState, useMemo, useEffect } from "react" +import { useGlobalContext } from "@common/contexts/GlobalStateContext" +import { useNavigate } from "react-router-dom" + +type TableBtnWithPermissionProps = { + btnTitle:string + access:string, + tooltip?:string, + disabled?:boolean, + navigateTo?:string, + onClick?:(args?:unknown)=>void + className?:string +} +// 表格操作栏按钮,受权限控制 +const TableBtnWithPermission = ({btnTitle, access, tooltip, disabled, navigateTo, onClick,className}:TableBtnWithPermissionProps) => { + + const [btnAccess, setBtnAccess] = useState(false) + const {accessData,checkPermission} = useGlobalContext() + const navigate = useNavigate() + const lastAccess = useMemo(()=>{ + if(!access) return true + return checkPermission(access) + },[access, accessData]) + + useEffect(()=>{ + access ? setBtnAccess(lastAccess) : setBtnAccess(true) + },[]) + + return (<>{ + !btnAccess || (disabled&&tooltip) ? + + + + : + + + } + ); + } + +export default TableBtnWithPermission \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/TagWithPermission.tsx b/frontend/packages/common/src/components/aoplatform/TagWithPermission.tsx new file mode 100644 index 00000000..cbc4e2f3 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/TagWithPermission.tsx @@ -0,0 +1,37 @@ + +import { Tag, TagProps } from "antd"; +import { useState, useMemo, useEffect } from "react"; +import { PERMISSION_DEFINITION } from "@common/const/permissions"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext"; + +export interface TagWithPermission extends TagProps{ + access?:string +} +export default function TagWithPermission(props:TagWithPermission){ + const {access,onClose} = props + const [editAccess, setEditAccess] = useState(access ? false:true) + const {accessData,checkPermission} = useGlobalContext() + const lastAccess = useMemo(()=>{ + if(!access) return true + return checkPermission(access as keyof typeof PERMISSION_DEFINITION[0]) + },[access, accessData,checkPermission]) + + useEffect(()=>{ + access ? setEditAccess(lastAccess) : setEditAccess(true) + },[lastAccess]) + + const handleTagClose = (e: React.MouseEvent)=>{ + e.preventDefault(); + if(!editAccess) return + onClose?.(e) + } + + return + {props.children} + + +} \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/ThemeSwitcher.tsx b/frontend/packages/common/src/components/aoplatform/ThemeSwitcher.tsx new file mode 100644 index 00000000..367bea96 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/ThemeSwitcher.tsx @@ -0,0 +1,34 @@ + +import React, { useEffect, useState } from 'react'; + +const ThemeSwitcher = () => { + const [darkMode, setDarkMode] = useState(true); + + useEffect(() => { + let isDarkMode = localStorage.getItem('dark-mode'); + if(isDarkMode !== undefined && isDarkMode !== null){ + setDarkMode(isDarkMode === 'true') + }else{ + localStorage.setItem('dark-mode', (darkMode).toString()); + } + }, []); + + useEffect(()=>{ + document.documentElement.classList.toggle('dark', darkMode); + },[darkMode]) + + const toggleDarkMode = () => { + setDarkMode(!darkMode); + localStorage.setItem('dark-mode', (!darkMode).toString()); + document.documentElement.classList.toggle('dark', !darkMode); + }; + + return ( + // + <> + ); +}; + +export default ThemeSwitcher; \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/TimePicker.tsx b/frontend/packages/common/src/components/aoplatform/TimePicker.tsx new file mode 100644 index 00000000..ef440faf --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/TimePicker.tsx @@ -0,0 +1,16 @@ + +import {forwardRef} from 'react'; +import type { PickerProps } from 'antd/es/date-picker/generatePicker'; +import type { Moment } from 'moment'; + +import DatePicker from './DatePicker'; + +export interface TimePickerProps extends Omit, 'picker'> {} + +const TimePicker = forwardRef((props, ref) => ( + +)); + +TimePicker.displayName = 'TimePicker'; + +export default TimePicker; \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/TimeRangeSelector.tsx b/frontend/packages/common/src/components/aoplatform/TimeRangeSelector.tsx new file mode 100644 index 00000000..e66dd9c2 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/TimeRangeSelector.tsx @@ -0,0 +1,120 @@ + +import { useState } from 'react'; +import { Radio, DatePicker, GetProps, RadioChangeEvent } from 'antd'; +import dayjs, { Dayjs } from 'dayjs'; +import customParseFormat from 'dayjs/plugin/customParseFormat'; +import "../../index.css" + +type RangePickerProps = GetProps; +export type RangeValue = [Dayjs | null, Dayjs | null] | null; + +dayjs.extend(customParseFormat); + +export type TimeRange = { + start:number|null + end:number|null +} + +export type TimeRangeButton = ''| 'hour' | 'day' | 'threeDays' | 'sevenDays'; + +type TimeRangeSelectorProps = { + initialTimeButton?:TimeRangeButton, + initialDatePickerValue?:RangeValue + onTimeRangeChange?:(timeRange:TimeRange) =>void + hideTitle?:boolean + onTimeButtonChange:(time:TimeRangeButton) =>void + labelSize?:'small'|'default' + } +const TimeRangeSelector = (props:TimeRangeSelectorProps) => { + const {initialTimeButton,initialDatePickerValue,onTimeRangeChange,hideTitle,onTimeButtonChange,labelSize='default'} = props + const [timeButton, setTimeButton] = useState(initialTimeButton || ''); + const [datePickerValue, setDatePickerValue] = useState(initialDatePickerValue || [null,null]); + + // 根据选择的时间范围计算开始和结束时间 + const calculateTimeRange = (curBtn:'hour'|'day'|'threeDays'|'sevenDays') => { + const currentSecond = new Date().getTime() // 当前毫秒数时间戳 + const currentMin = currentSecond - (currentSecond % (60 * 1000)) // 当前分钟数时间戳 + let startMin = currentMin - 60 * 60 * 1000 + switch (curBtn) { + case 'hour': { + startMin = currentMin - 60 * 60 * 1000 + break + } + case 'day': { + startMin = currentMin - 24 * 60 * 60 * 1000 + break + } + case 'threeDays': { + startMin = + new Date(new Date().setHours(0, 0, 0, 0)).getTime() - + 2 * 24 * 60 * 60 * 1000 + break + } + case 'sevenDays': { + startMin = + new Date(new Date().setHours(0, 0, 0, 0)).getTime() - + 6 * 24 * 60 * 60 * 1000 + break + } + } + if (onTimeRangeChange) { + onTimeRangeChange({ start: startMin / 1000, end: currentMin / 1000 }); + } + }; + + // 处理单选按钮的变化 + const handleRadioChange = (e:RadioChangeEvent) => { + setTimeButton(e.target.value); + onTimeButtonChange?.(e.target.value) + setDatePickerValue(null) + calculateTimeRange(e.target.value); + }; + + // 处理日期选择器的变化 + const handleDatePickerChange = (dates: RangeValue) => { + setTimeButton(dates ? '' : 'hour') + onTimeButtonChange?.(dates ? '' : 'hour') + setDatePickerValue(dates); + if (dates && Array.isArray(dates) && dates.length === 2) { + const [startDate, endDate] = dates; + const start = startDate!.startOf('day').unix(); // 开始日期的00:00:00 + const end = endDate!.endOf('day').unix(); // 结束日期的23:59:59 + if (onTimeRangeChange) { + onTimeRangeChange({ start, end }); + } + } + }; + + + +const disabledDate: RangePickerProps['disabledDate'] = (current) => { +// Can not select days before today and today + return current && current.valueOf() > dayjs().startOf('day').valueOf(); +}; + + return ( +
+ {!hideTitle && } + + 近1小时 + 近24小时 + 近3天 + 近7天 + + { + if(!open && datePickerValue && datePickerValue.length > 2){ + setTimeButton('') + onTimeButtonChange?.('') + } + }} + /> +
+ ); +}; + +export default TimeRangeSelector; \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/TransferTable.module.css b/frontend/packages/common/src/components/aoplatform/TransferTable.module.css new file mode 100644 index 00000000..53fdaa90 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/TransferTable.module.css @@ -0,0 +1,28 @@ +.transfer-table-member, +.transfer-table-api{ + :global .ant-table-wrapper .ant-table-thead >tr>th, + :global .ant-table-wrapper .ant-table-thead >tr>td{ + font-weight: normal; + background-color: var(--MAIN_BG); + border:none; + } + + :global .ant-table-wrapper .ant-table-tbody-virtual .ant-table-cell{ + border:none; + } + + :global .rc-virtual-list-scrollbar.rc-virtual-list-scrollbar-horizontal{ + display: none; + } + + :global .ant-table-wrapper .ant-table-tbody .ant-table-row > .ant-table-cell-row-hover{ + background: #EBEEF2; + } + + :global .ant-table-wrapper .ant-table-tbody .ant-table-row.ant-table-row-selected > .ant-table-cell-row-hover{ + background: #EBEEF2; + } + :global .ant-table-thead >tr>th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before{ + display: none; + } +} \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/TransferTable.tsx b/frontend/packages/common/src/components/aoplatform/TransferTable.tsx new file mode 100644 index 00000000..69bbc141 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/TransferTable.tsx @@ -0,0 +1,193 @@ + +import { Input,Table} from "antd"; +import {forwardRef, KeyboardEventHandler, Ref, useCallback, useEffect, useImperativeHandle, useRef, useState} from "react"; +import styles from './TransferTable.module.css' +import {CloseOutlined, SearchOutlined} from "@ant-design/icons"; +import {debounce} from "lodash-es"; +import {ColumnsType} from "antd/es/table"; + +export type TransferTableProps = { + request?:(k?:string)=>Promise<{data:T[],success:boolean}> + columns: ColumnsType + primaryKey:string + onSelect:(selectedData:T[])=>void + tableType?:'member'|'api' + disabledData:string[] + searchPlaceholder?:string +} + +export type TransferTableHandle = { + selectedData: () => T[]; + selectedRowKeys: () => React.Key[]; +} + +const TransferTable = forwardRef, TransferTableProps<{[k:string]:unknown}>>( + (props: TransferTableProps, ref:Ref>) => { + const {request,columns,primaryKey,onSelect,tableType,disabledData = [],searchPlaceholder} = props + const tblRef: Parameters[0]['ref'] = useRef(null); + const [selectedRowKeys, setSelectedRowKeys] = useState(disabledData); + const [selectedData, setSelectedData] = useState>([]) + const [dataSource, setDataSource] = useState([]) + const [searchWord, ] = useState('') + const [loading, setLoading] = useState(false) + const parentRef = useRef(null); + const [tableHeight, setTableHeight] = useState(window.innerHeight * 80 / 100 ); + const [tableShow, setTableShow] = useState(false); + + useImperativeHandle(ref, () =>({ + selectedData: () => selectedData, + selectedRowKeys: () => selectedRowKeys,})) + + const handlerLeftTableClick = (record:T & {[k:string]:string})=>{ + if(disabledData.indexOf(record[primaryKey||'id' as string] )!== -1) return + + const tmpSelectedRowKeys = [...selectedRowKeys]; + if (tmpSelectedRowKeys.indexOf(record[primaryKey||'id' as string]) !== -1) { + tmpSelectedRowKeys.splice(tmpSelectedRowKeys.indexOf(record[primaryKey||'id' as string]), 1); + } else { + tmpSelectedRowKeys.push(record[primaryKey||'id' as string]); + } + setSelectedRowKeys(tmpSelectedRowKeys); + let tmpSelectedData = [...selectedData] + if(tmpSelectedData.filter((x: T)=>x[primaryKey || 'id'] === record[primaryKey||'id' as string]).length > 0){ + tmpSelectedData = tmpSelectedData.filter((x: T)=>x[primaryKey || 'id'] !== record[primaryKey||'id' as string]) + }else{ + tmpSelectedData.push(record) + } + setSelectedData(tmpSelectedData) + } + + // const handlerRightTableClick = (record:T & {[k:string]:string})=>{ + // const tmpSelectedRowKeys = [...selectedRowKeys]; + // if (tmpSelectedRowKeys.indexOf(record[primaryKey||'id' as string]) >= 0) { + // tmpSelectedRowKeys.splice(tmpSelectedRowKeys.indexOf(record[primaryKey||'id' as string]), 1); + // } + // setSelectedRowKeys(tmpSelectedRowKeys); + // let tmpSelectedData = [...selectedData] + // tmpSelectedData = tmpSelectedData.filter((x: {[k:string]:string})=>x[primaryKey || 'id'] !== record[primaryKey||'id' as string]) + // setSelectedData(tmpSelectedData) + // } + + const onSelectChange = (newSelectedRowKeys: React.Key[], selectedRow:T[]) => { + setSelectedRowKeys(newSelectedRowKeys); + setSelectedData(selectedRow.filter((x:T)=>disabledData.indexOf(x[primaryKey || 'id'] as string) === -1)) + }; + const removeItem = (item:T )=>{ + setSelectedRowKeys(selectedRowKeys.filter((x)=>{return x!==item[primaryKey || 'id']})) + setSelectedData((prevData)=>prevData.filter((x:T)=>(x[primaryKey || 'id'] !== item[primaryKey || 'id']))) + } + + useEffect(() => { + onSelect && onSelect(selectedData) + }, [selectedData]); + + const operations = [ + { + title: '操作', + key: 'option', + width: 40, + valueType: 'option', + render: (_: React.ReactNode, entity: T) => [ + removeItem(entity as T)}/> + ], + } + ] + + const onSearchWordChange = (e: KeyboardEventHandler)=>{ + getDataSource(e.target.value) + } + + const getDataSource = (curSearchWord?:string)=>{ + setLoading(true) + request && request(curSearchWord ?? searchWord).then((res)=>{ + const {data,success} = res + setDataSource(success? data : []) + }).finally(()=>{setLoading(false)}) + } + + const debouncedSearch = useCallback( + debounce(onSearchWordChange, 600),[] + ) + + + useEffect(() => { + getDataSource() + const handleResize = () => { + if (parentRef.current) { + const res = parentRef.current.getBoundingClientRect(); + setTableHeight(res.height - 32 - 12 * 2 -42 -2) + setTimeout(()=>setTableShow(true),100) + // setTableWidth(res.width / 2 - 20 - 2 - 40) + // const height = res.height -( noTop ? 0 :52) - (dragSortKey ? 0 : 53) - 40;// 减去顶部按钮、底部分页、表头高度 + // height && setTableHeight(height); + } + }; + + const debouncedHandleResize = debounce(handleResize, 200); + + // 创建一个 ResizeObserver 来监听高度变化 + const resizeObserver = new ResizeObserver(debouncedHandleResize); + + // 开始监听 + if (parentRef.current ) { + resizeObserver.observe(parentRef.current); + } + + // 清理函数 + return () => { + resizeObserver.disconnect(); + }; + }, []); + return ( +
+
+ {onSearchWordChange}}/>} /> + {tableShow &&
({ + disabled: disabledData.length > 0 && disabledData?.indexOf(record[primaryKey || 'id'] as string) !== -1, // Column configuration not to be checked + name: record[primaryKey || 'id'], + + }), + } + } + onRow={(record) => ({ + onClick: () => { + handlerLeftTableClick(record); + } + })} + />} + +
+
+ 已选{tableType === 'member' ? '成员' : ' API'} ({selectedData.length}) +
+
({...col,className:(col.className || ' ') + 'pl-[20px]'})),...operations]} + showHeader={false} + rowKey={primaryKey} + dataSource={selectedData} + pagination={false} + ref={tblRef} + loading={loading} + /> + + ); +}) +export default TransferTable \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/TreeWithMore.tsx b/frontend/packages/common/src/components/aoplatform/TreeWithMore.tsx new file mode 100644 index 00000000..e2f41992 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/TreeWithMore.tsx @@ -0,0 +1,45 @@ + +import {CheckOutlined, LoadingOutlined, MoreOutlined} from "@ant-design/icons"; +import {Dropdown, Input, InputRef, MenuProps} from "antd"; +import { ReactNode, useEffect, useRef, useState} from "react"; + +export type TreeWithMoreProp = { + children:ReactNode, + dropdownMenu:MenuProps['items'] + editable?:boolean + editingId?:string + afterEdit?:(val:string)=>Promise + editKey?:string + entity?:{id:string,[k:string]:unknown | string} + onBlur?:()=>void + stopClick?:boolean +} + +const TreeWithMore = ({children,dropdownMenu,editable,editingId,entity,editKey='name',afterEdit,onBlur,stopClick=true}:TreeWithMoreProp)=>{ + const [editValue, setEditValue] = useState(entity?.[editKey] as string) + const [submitting, setSubmitting] = useState(false) + const inputRef = useRef(null) + + const handleSubmit = (val:string)=>{ + if(submitting) return + setSubmitting(true) + afterEdit && afterEdit(val).finally(()=>setSubmitting(false)) + } + + useEffect(()=>{inputRef.current?.focus()},[inputRef]) + + return (<> + { + editable && editingId && entity?.id && editingId === entity.id ? {setEditValue(e.target.value)}} onBlur={()=>{onBlur?.()}} onClick={(e)=>stopClick&&e?.stopPropagation()} onPressEnter={()=>{handleSubmit(editValue)}} suffix={submitting ? :{handleSubmit(editValue)}}/>} />: + +
{children} + { stopClick && e.stopPropagation();}}> + + { stopClick && e.stopPropagation(); }} /> + + +
+
+ }) +} +export default TreeWithMore \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/UserAvatar.tsx b/frontend/packages/common/src/components/aoplatform/UserAvatar.tsx new file mode 100644 index 00000000..7748c443 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/UserAvatar.tsx @@ -0,0 +1,120 @@ +import {App, Avatar, Dropdown, MenuProps} from "antd"; +import {useGlobalContext} from "@common/contexts/GlobalStateContext.tsx"; +import {FC, useEffect, useRef, useState} from "react"; +import {ResetPsw, ResetPswHandle} from "@common/components/aoplatform/ResetPsw.tsx"; +import {useFetch} from "@common/hooks/http.ts"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useNavigate} from "react-router-dom"; +import { UserInfoType, UserProfileHandle } from "@common/const/type.ts"; +import { UserProfile } from "./UserProfile.tsx"; +import AvatarPic from '@common/assets/avatar_default.svg' + +const UserAvatar: FC = () => { + const { modal,message } = App.useApp() + const { dispatch,resetAccess,getGlobalAccessData} = useGlobalContext() + const [userInfo,setUserInfo] = useState() + const resetPswRef = useRef(null) + const userProfileRef = useRef(null) + const {fetchData} = useFetch() + const navigate = useNavigate(); + + const getUserInfo = ()=>{ + fetchData>('account/profile',{method:'GET'}) + .then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setUserInfo(data.profile) + dispatch({type:'UPDATE_USERDATA',userData:data.profile}) + }else{ + message.error(msg || '操作失败') + } + }) + } + + useEffect(() => { + getUserInfo() + getGlobalAccessData() + }, []); + + const logOut = ()=>{ + fetchData>('account/logout',{method:'GET'}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + dispatch({type:'LOGOUT'}) + resetAccess() + message.success(msg || '退出成功,将跳转至登录页') + navigate('/login') + }else{ + message.error(msg ||'操作失败') + } + }) + } + + const items: MenuProps['items'] = [ + // { + // key: '1', + // label: ( + // openModal('userSetting')}> + // 用户设置 + // + // ), + // }, + // { + // key: '2', + // label: ( + // openModal('resetPsw')}> + // 修改密码 + // + // ), + // }, + { + key: '3', + label: ( + + 退出登录 + + ), + }, + ]; + + const openModal = (type:'userSetting'|'resetPsw')=>{ + let title:string = '' + let content:string|React.ReactNode = '' + switch (type){ + case 'userSetting': + title='用户设置' + content= + break; + case 'resetPsw': + title='重置密码' + content= + break; + } + modal.confirm({ + title, + content, + onOk:()=>{ + switch (type){ + case 'userSetting': + return userProfileRef.current?.save().then((res)=>{if(res === true) getUserInfo()}) + case 'resetPsw': + return resetPswRef.current?.save().then((res)=>{if(res === true) logOut()}) + } + }, + width:600, + okText:'确认', + cancelText:'取消', + closable:true, + icon:<>, + }) + } + + + return ( + + {userInfo?.username||'unknown'} + + ) +} + +export default UserAvatar \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/UserProfile.tsx b/frontend/packages/common/src/components/aoplatform/UserProfile.tsx new file mode 100644 index 00000000..c6b98f7a --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/UserProfile.tsx @@ -0,0 +1,152 @@ +import {App, Form, Input, Upload, UploadFile, UploadProps} from "antd"; +import {forwardRef, useEffect, useImperativeHandle, useState} from "react"; +import {useFetch} from "@common/hooks/http.ts"; +import {RcFile, UploadChangeParam} from "antd/es/upload"; +import {LoadingOutlined} from "@ant-design/icons"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import { UserInfoType, UserProfileHandle, UserProfileProps } from "@common/const/type"; +import { getImgBase64 } from "@common/utils/dataTransfer"; +import { Icon } from "@iconify/react/dist/iconify.js"; + +export const UserProfile = forwardRef((props,ref)=>{ + const { message } = App.useApp() + const [form] = Form.useForm(); + const {entity,} = props + const {fetchData} = useFetch() + const [imageBase64, setImageBase64] = useState(null); + const [loading, setLoading] = useState(false); + const [imageUrl, setImageUrl] = useState(); + + const save:()=>Promise = ()=>{ + return new Promise((resolve, reject)=>{ + form.validateFields().then((value)=>{ + fetchData>('account/profile',{method:'PUT',eoBody:value}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + useImperativeHandle(ref, ()=>({ + save + }) + ) + + + const handleChange: UploadProps['onChange'] = (info: UploadChangeParam) => { + if (info.file.status === 'uploading') { + setLoading(true); + return; + } + if (info.file.status === 'done') { + // Get this url from response in real world. + getImgBase64(info.file.originFileObj as RcFile, (url) => { + setLoading(false); + setImageUrl(url); + }); + } + if (info.fileList.length === 0) { + // 如果文件被移除,清除 logo 字段 + form.setFieldValue( "avatar", null ); + } + }; + + const uploadButton = ( +
+
+ {loading ? : }
+
+ ); + + const beforeUpload = (file: RcFile) => { + const reader = new FileReader(); + reader.onload = (e: ProgressEvent) => { + setImageBase64(e.target?.result as string); + form.setFieldValue("avatar",e.target?.result) + }; + reader.readAsDataURL(file); + return false; + }; + + useEffect(() => { + form.setFieldsValue(entity) + }, []); + + +const normFile = (e: unknown) => { + if (Array.isArray(e)) { + return e; + } + return( e as {fileList:unknown} )?.fileList; + }; + return (<> +
+ + label="账号" + name="username" + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + + + + label="昵称" + name="nickname" + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + + + + label="头像" + name="avatar" + valuePropName="fileList" getValueFromEvent={normFile} + > + + {imageBase64 ? Logo : uploadButton} + + + + + + label="邮箱" + name="email" + rules={[{ required: true, message: '必填项' ,whitespace:true },{type:'email',message: '输入的不是有效邮箱格式'}]} + > + + + + + label="手机号码" + name="phone" + rules={[{pattern:/^(13[0-9]|14[5|7]|15[0|1|2|3|4|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$/, message:'输入的不是有效手机号码',warningOnly: true }]} + > + + + + + ) +}) \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/WithPermission.tsx b/frontend/packages/common/src/components/aoplatform/WithPermission.tsx new file mode 100644 index 00000000..c094cd55 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/WithPermission.tsx @@ -0,0 +1,41 @@ + +import { Tooltip } from "antd"; +import { ReactElement, cloneElement, useEffect, useMemo, useState } from "react"; +import { useGlobalContext } from "../../contexts/GlobalStateContext"; +import { PERMISSION_DEFINITION } from "@common/const/permissions"; + +type WithPermissionProps = { + access?:string | string[] + tooltip?:string + children:ReactElement + disabled?:boolean +} +// 权限控制的高阶组件 +const WithPermission = ({access, tooltip, children,disabled}:WithPermissionProps) => { + + const [editAccess, setEditAccess] = useState(access ? false:true) + const {accessData,checkPermission} = useGlobalContext() + + const lastAccess = useMemo(()=>{ + if(!access) return true + return checkPermission(access as keyof typeof PERMISSION_DEFINITION[0]) + },[access, accessData]) + + useEffect(()=>{ + // 先判断权限,无论权限是否为true,如果disabled为true时则必须为ture + access && setEditAccess(lastAccess) + disabled && setEditAccess(false) + },[lastAccess,disabled]) + + return ( + <> + {editAccess ? cloneElement(children): + + { cloneElement(children, {disabled:true})} + + } + + ); + } + +export default WithPermission \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/formily2-customize/ArrayItemBlankComponent.tsx b/frontend/packages/common/src/components/aoplatform/formily2-customize/ArrayItemBlankComponent.tsx new file mode 100644 index 00000000..2e573d39 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/formily2-customize/ArrayItemBlankComponent.tsx @@ -0,0 +1,167 @@ + +import {forwardRef, useImperativeHandle, useState} from 'react' + +import { Input } from 'antd' +export const ArrayItemBlankComponent = forwardRef( + (props: { [k: string]: any }, ref) => { + const { onChange, value, dataFormat } = props + + const getDefaultListItem = () => { + const defaultData: { [k: string]: unknown } = {} + + for (const data of dataFormat) { + defaultData[data.key] = '' + } + + return [defaultData] + } + + const [resList, setResList] = useState( + value && Object.keys(value).length > 0 + ? [ + ...value + ?.filter((v: string) => { + return v + }) + ?.map((v: string) => { + const vTmp = v + const newValue: { [k: string]: unknown } = {} + for (let index = 0; index < dataFormat.length; index++) { + if (dataFormat[index]?.hideName) { + newValue[dataFormat[index].key] = vTmp.split(' ')[index] + } else { + const vTmp2: string | string[] | undefined = + vTmp.indexOf(' ') === -1 + ? vTmp + : vTmp.split(' ')[index] + ? vTmp.split(' ')[index].indexOf('=') === -1 + ? '' + : vTmp.split(' ')[index].split('=') + : '' + + if (vTmp2 && vTmp2 instanceof Array && vTmp2.length > 0) { + vTmp2.shift() + } + newValue[dataFormat[index].key] = + vTmp2 instanceof Array ? vTmp2?.join('=') : vTmp2 + } + } + return newValue + }), + ...getDefaultListItem() + ] + : [...getDefaultListItem()] + ) + + useImperativeHandle(ref, () => ({})) + + const emitNewArr = () => { + const newArr: Array = [] + for (const r of resList) { + if (r[dataFormat[0].key]) { + newArr.push( + dataFormat?.map((format: { key: string; hideName: boolean }) => { + return format?.hideName + ? r[format.key] + : `${format.key}=${r[format.key]}` + }) + .join(' ') + ) + } + } + onChange(newArr) + } + + const changeInputValue = ( + newValue: string, + index: number, + keyName: string, + dataFormat: unknown + ) => { + const newArr = [...resList] + newArr[index][keyName] = newValue + newArr[index].status = + (dataFormat.required && !newValue) || + (dataFormat.pattern && !dataFormat.pattern.test(newValue)) + ? 'error' + : '' + setResList(newArr) + emitNewArr() + if (index === resList.length - 1) { + setResList([...newArr, ...getDefaultListItem()]) + } + } + + const addLine = (index: number) => { + resList.splice(index + 1, 0, ...getDefaultListItem()) + const newKvList = [...resList] + setResList(newKvList) + emitNewArr() + } + + const removeLine = (index: number) => { + resList.splice(index, 1) + const newKvList = [...resList] + setResList([...newKvList]) + emitNewArr() + } + + return ( +
+ {resList?.map((n: unknown, index: unknown) => { + return ( +
+ {dataFormat?.map((data: unknown, index2: unknown) => { + return ( + { + changeInputValue(e.target.value, index, data.key, data) + }} + placeholder={data.placeholder || `请输入${data.key}`} + status={n.status} + type={data.type || 'text'} + /> + ) + })} + + {index !== resList.length - 1 && ( + + )} +
+ ) + })} +
+ ) + } +) diff --git a/frontend/packages/common/src/components/aoplatform/formily2-customize/CustomCodeboxComponent.tsx b/frontend/packages/common/src/components/aoplatform/formily2-customize/CustomCodeboxComponent.tsx new file mode 100644 index 00000000..43853bb4 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/formily2-customize/CustomCodeboxComponent.tsx @@ -0,0 +1,47 @@ + +import {forwardRef, useImperativeHandle, useState} from 'react' +import { Codebox } from '@common/components/postcat/api/Codebox' + +export const CustomCodeboxComponent = forwardRef( + (props: { [k: string]: unknown }, ref) => { + const { + mode = 'yaml', + theme = 'xcode', + fontSize, + height, + width = '100%', + onChange, + value + } = props + const [code, setCode] = useState( + mode === 'json' ? JSON.stringify(value) : value + ) + useImperativeHandle(ref, () => ({})) + const handleChange = (value: string) => { + setCode(value) + let res = value + if (mode === 'json') { + try { + res = JSON.parse(value) + } catch { + console.warn(' 输入的json语句格式有误') + } + } + onChange(res) + } + + return ( +
+ +
+ ) + } +) diff --git a/frontend/packages/common/src/components/aoplatform/formily2-customize/CustomDialogComponent.tsx b/frontend/packages/common/src/components/aoplatform/formily2-customize/CustomDialogComponent.tsx new file mode 100644 index 00000000..938fac75 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/formily2-customize/CustomDialogComponent.tsx @@ -0,0 +1,137 @@ + +import {forwardRef,useImperativeHandle} from 'react' +import { createSchemaField } from '@formily/react' +import { + FormItem, + Space, + ArrayItems, + DatePicker, + Editable, + FormButtonGroup, + Input, + Radio, + Select, + Submit, + Cascader, + Form, + FormGrid, + FormLayout, + Upload, + ArrayCollapse, + ArrayTable, + ArrayTabs, + Checkbox, + FormCollapse, + FormDialog, + FormDrawer, + FormStep, + FormTab, + NumberPicker, + Password, + PreviewText, + Reset, + SelectTable, + Switch, + TimePicker, + Transfer, + TreeSelect, + ArrayCards +} from '@formily/antd-v5' +import { CustomCodeboxComponent } from './CustomCodeboxComponent.tsx' +import { SimpleMapComponent } from './SimpleMapComponent.tsx' + +const SchemaField = createSchemaField({ + components: { + ArrayCards, + ArrayCollapse, + ArrayItems, + ArrayTable, + ArrayTabs, + Cascader, + Checkbox, + DatePicker, + Editable, + Form, + FormButtonGroup, + FormCollapse, + FormDialog, + FormDrawer, + FormGrid, + FormItem, + FormLayout, + FormStep, + FormTab, + Input, + NumberPicker, + Password, + PreviewText, + Radio, + Reset, + Select, + SelectTable, + Space, + Submit, + Switch, + TimePicker, + Transfer, + TreeSelect, + Upload, + CustomCodeboxComponent, + SimpleMapComponent + } +}) + +export const CustomDialogComponent = forwardRef( + (props: { [k: string]: unknown }, ref) => { + const { onChange, title, value, render } = props + useImperativeHandle(ref, () => ({})) + let editPage: boolean = false + try { + editPage = Object.keys(JSON.parse(JSON.stringify(value))).length > 0 + } catch {} + + return ( + + { + const dialog = FormDialog( + editPage ? `编辑${title || ''}` : `添加${title || ''}`, + () => { + return ( + + + + ) + } + ) + dialog + .forOpen((payload, next) => { + next({ + initialValues: value + }) + }) + .forConfirm((payload, next) => { + next(payload) + }) + .forCancel((payload, next) => { + next(payload) + }) + .open() + .then(onChange) + }} + > + + + + + + ) + } +) diff --git a/frontend/packages/common/src/components/aoplatform/formily2-customize/SimpleMapComponent.tsx b/frontend/packages/common/src/components/aoplatform/formily2-customize/SimpleMapComponent.tsx new file mode 100644 index 00000000..6d82f0bb --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/formily2-customize/SimpleMapComponent.tsx @@ -0,0 +1,120 @@ +import {forwardRef, useImperativeHandle, useState} from 'react' + +import { Input } from '@formily/antd-v5' +export const SimpleMapComponent = forwardRef( + (props: { [k: string]: unknown }, ref) => { + const { + onChange, + value, + placeholderKey = '请输入Key', + placeholderValue = '请输入Value' + } = props + + const [kvList, setKvList] = useState( + value && Object.keys(value).length > 0 + ? [ + ...Object.keys(value)?.map((k: string) => { + return { key: k, value: value[k] } + }), + { key: '', value: '' } + ] + : [{ key: '', value: '' }] + ) + + useImperativeHandle(ref, () => ({})) + + const emitNewArr = () => { + const res: { [k: string]: unknown } = {} + for (const kv of kvList) { + res[kv.key] = kv.value + } + onChange(res) + } + + const changeInputValue = ( + newValue: string, + index: number, + type: 'key' | 'value' + ) => { + const newArr = [...kvList] + newArr[index][type] = newValue + setKvList(newArr) + emitNewArr() + if (index === kvList.length - 1) { + setKvList([...newArr, { key: '', value: '' }]) + } + } + + const addLine = (index: number) => { + kvList.splice(index + 1, 0, { key: '', value: '' }) + const newKvList = [...kvList] + setKvList(newKvList) + emitNewArr() + } + + const removeLine = (index: number) => { + kvList.splice(index, 1) + const newKvList = [...kvList] + setKvList(newKvList) + emitNewArr() + } + + return ( +
+ {kvList?.map((n: unknown, index: unknown) => { + return ( +
+ { + changeInputValue(e.target.value, index, 'key') + }} + placeholder={placeholderKey} + /> + { + changeInputValue(e.target.value, index, 'value') + }} + placeholder={placeholderValue} + /> + {index !== kvList.length - 1 && ( + + )} +
+ ) + })} +
+ ) + } +) diff --git a/frontend/packages/common/src/components/aoplatform/intelligent-plugin/IntelligentPluginConfig.tsx b/frontend/packages/common/src/components/aoplatform/intelligent-plugin/IntelligentPluginConfig.tsx new file mode 100644 index 00000000..9f9c5a74 --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/intelligent-plugin/IntelligentPluginConfig.tsx @@ -0,0 +1,291 @@ +import {forwardRef, useEffect, useImperativeHandle, useState} from "react"; +import { action } from '@formily/reactive' +import { + FormItem, + Space, + ArrayItems, + DatePicker, + Editable, + FormButtonGroup, + Input, + Radio, + Select, + Submit, + Cascader, + Form, + FormGrid, + FormLayout, + Upload, + ArrayCollapse, + ArrayTable, + ArrayTabs, + Checkbox, + FormCollapse, + FormDialog, + FormDrawer, + FormStep, + FormTab, + NumberPicker, + Password, + PreviewText, + Reset, + SelectTable, + Switch, + TimePicker, + Transfer, + TreeSelect, + ArrayCards +} from '@formily/antd-v5' +import { createForm } from '@formily/core' +import {CustomCodeboxComponent} from "@common/components/aoplatform/formily2-customize/CustomCodeboxComponent.tsx"; +import {SimpleMapComponent} from "@common/components/aoplatform/formily2-customize/SimpleMapComponent.tsx"; +import {CustomDialogComponent} from "@common/components/aoplatform/formily2-customize/CustomDialogComponent.tsx"; +import {ArrayItemBlankComponent} from "@common/components/aoplatform/formily2-customize/ArrayItemBlankComponent.tsx"; +import {DefaultOptionType} from "antd/es/cascader"; +import {createSchemaField, FormProvider, RecursionField, useField, useForm} from "@formily/react"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import {App} from "antd"; + + + +export const DynamicRender = (props) => { + const {schema} = props + const field = useField() + const form = useForm() + const [renderSchema, setRenderSchema] = useState({}) + + useEffect(() => { + form.clearFormGraph(`${field.address}.*`) + try{ + const parsedSchema = JSON.parse(schema) + setRenderSchema(parsedSchema[form?.values?.driver]) + }catch(e){ + console.error('渲染出错',e?.message) + } + }, [form.values.driver]) + + return ( + + ) +} + + + +export type IntelligentPluginConfigProps = { + type:'add'|'edit' + renderSchema:unknown + tabData:DefaultOptionType[] + moduleId:string + driverSelectionOptions:DefaultOptionType[] + entityId?:string + initFormValue:{[k:string]:unknown} +} + +export type IntelligentPluginConfigHandle = { + save:()=>Promise +} + +const SchemaField = createSchemaField({ + components: { + ArrayCards, + ArrayCollapse, + ArrayItems, + ArrayTable, + ArrayTabs, + Cascader, + Checkbox, + DatePicker, + Editable, + Form, + FormButtonGroup, + FormCollapse, + // @ts-ignore + FormDialog, + // @ts-ignore + FormDrawer, + FormGrid, + FormItem, + FormLayout, + FormStep, + FormTab, + Input, + NumberPicker, + Password, + PreviewText, + Radio, + Reset, + Select, + SelectTable, + Space, + Submit, + Switch, + TimePicker, + Transfer, + TreeSelect, + Upload, + CustomCodeboxComponent, + SimpleMapComponent, + CustomDialogComponent, + ArrayItemBlankComponent, + DynamicRender + } +}) + +export const IntelligentPluginConfig = forwardRef((props,ref)=>{ + const { type,renderSchema,moduleId,driverSelectionOptions,initFormValue} = props + const { message } = App.useApp() + const {fetchData} = useFetch() + const form = createForm({ validateFirst: type === 'edit' }) + form.setInitialValues(initFormValue || {}) + + const pluginEditSchema = { + type: 'object', + properties: { + layout: { + type: 'void', + 'x-component': 'FormLayout', + 'x-component-props': { + labelCol: 6, + wrapperCol: 10, + layout: 'vertical', + }, + properties: { + id: { + type: 'string', + title: 'ID', + required: true, + pattern: /^[a-zA-Z][a-zA-Z0-9-_]*$/, + 'x-decorator': 'FormItem', + 'x-decorator-props': { + labelCol:4, + wrapperCol: 20, + labelAlign:'left' + }, + 'x-component': 'Input', + 'x-component-props': { + placeholder: '支持字母开头、英文数字中横线下划线组合', + }, + 'x-disabled': type === 'edit' + }, + title: { + type: 'string', + title: '名称', + required: true, + 'x-decorator': 'FormItem', + 'x-decorator-props': { + labelCol:4, + wrapperCol: 20, + labelAlign:'left' + }, + 'x-component': 'Input', + 'x-component-props': { + placeholder: '请输入名称', + } + }, + driver: { + type: 'string', + title: 'Driver', + required: true, + 'x-decorator': 'FormItem', + 'x-decorator-props': { + labelCol:4, + wrapperCol: 20, + labelAlign:'left' + }, + 'x-component': 'Select', + 'x-component-props': { + disabled: type === 'edit' + }, + 'x-display': driverSelectionOptions.length > 1 ? 'visible' : 'hidden', + enum: [...driverSelectionOptions] + }, + description: { + type: 'string', + title: '描述', + 'x-decorator': 'FormItem', + 'x-decorator-props': { + labelCol:4, + wrapperCol: 20, + labelAlign:'left' + }, + 'x-component': 'Input.TextArea', + 'x-component-props': { + placeholder: '请输入描述', + } + }, + container: { + type: 'void', + 'x-component': 'DynamicRender', + 'x-component-props': { + schema: JSON.stringify(renderSchema), + } + } + } + } + } +} + + + const save :()=>Promise = ()=>{ + return new Promise((resolve, reject)=>{ + form.validate().then(()=>{ + fetchData>(type === 'add'?`dynamic/${moduleId}`:`dynamic/${moduleId}/config`,{method:type === 'add'? 'POST' : 'PUT',eoBody:form.values, eoParams:{...(type !== 'add' && {id:initFormValue.id})}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }).catch((errorInfo:unknown)=> reject(errorInfo)) + }) + } + + useImperativeHandle(ref, ()=>({ + save + }) + ) + + + const getSkillData = async (skill: string) => { + return new Promise((resolve,reject) => { + fetchData}>>(`api/common/provider/${skill}`,{method:'GET'}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + resolve(data[skill]?.map((x:{name:string,title:string})=>{return{label:x.title, value:x.name}}) || []) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }) + }) + } + + const useAsyncDataSource = + (service: unknown, skill: string) => (field: unknown) => { + field.loading = true + service(skill).then( + action.bound && + action.bound((data: unknown) => { + field.dataSource = data + field.loading = false + }) + ) + } + return ( +
+ + + +
) +}) \ No newline at end of file diff --git a/frontend/packages/common/src/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx b/frontend/packages/common/src/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx new file mode 100644 index 00000000..4a244e0e --- /dev/null +++ b/frontend/packages/common/src/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx @@ -0,0 +1,346 @@ +import PageList from "@common/components/aoplatform/PageList.tsx"; +import {App, Divider, Spin} from "antd"; +import {useEffect, useRef, useState} from "react"; +import { useLocation, useOutletContext, useParams} from "react-router-dom"; +import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import {ActionType, ParamsType, ProColumns} from "@ant-design/pro-components"; +import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx"; +import {DefaultOptionType} from "antd/es/cascader"; +import {IntelligentPluginConfig, IntelligentPluginConfigHandle} from "./IntelligentPluginConfig.tsx"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import {EntityItem} from "@common/const/type.ts"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx"; +import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter.tsx"; +import { LoadingOutlined } from "@ant-design/icons"; + + type DynamicTableField = { + name: string, + title: string, + attr: string, + enum: Array +} + + type DynamicDriverData = { + name:string, title:string +} + +export type DynamicTableConfig = { + basic:{ + id:string, + name: string, + title: string, + drivers: Array, + fields: Array, + } + list: Array, + total:number +} + +export type DynamicRender = { + render:unknown, + basic:{ + id:string, + name:string, + title:string + } +} + +export type DynamicPublishCluster = { + name:string, + title:string, + status:string, + updater:EntityItem, + update_time:string, + checked?:boolean +} + +export type DynamicPublishData = { + id:string, + name:string, + title:string, + description:string + clusters:DynamicPublishCluster[] +} + +export type DynamicTableItem = {[k:string]:unknown} + +export const StatusColorClass = { + "已发布":'text-[#03a9f4]', + "待发布":'text-[#46BE11]', + "未发布":'text-[#03a9f4]' +} + + +export type DynamicPublish = { + code:number, + msg:string, + data:{ + success:Array, + fail:Array + } +} + +export default function IntelligentPluginList(){ + const { modal,message } = App.useApp() + const [searchWord, setSearchWord] = useState('') + const { moduleId } = useParams(); + const [pluginName,setPluginName] = useState('-') + const [partitionOptions] = useState([{label:'default', value:'default'}]) + const { setBreadcrumb } = useBreadcrumb() + const [renderSchema ,setRenderSchema] = useState<{[k:string]:unknown}>({}) + const drawerFormRef = useRef(null); + const [driverOptions, setDriverOptions] = useState([]) + const [tableListDataSource, setTableListDataSource] = useState([]); + + const [tableHttpReload, setTableHttpReload] = useState(true); + const [columns,setColumns] = useState[] >([]) + const {fetchData} = useFetch() + const pageListRef = useRef(null); + const [publishBtnLoading, setPublishBtnLoading] = useState(false) + const [curDetail,setCurDetail] = useState<{[k: string]: unknown;}|undefined>() + const [drawerType, setDrawerType] = useState<'add'|'edit'>('add') + const [drawerOpen, setDrawerOpen] = useState(false) + const [drawerLoading, setDrawerLoading] = useState(false) + const location = useLocation().pathname + const {accessPrefix} = useOutletContext<{accessPrefix:string}>() + + + const getIntelligentPluginTableList=(params:ParamsType & { + pageSize?: number | undefined; + current?: number | undefined; + keyword?: string | undefined; + }): Promise<{ data: DynamicTableItem[], success: boolean }>=> { + if(!tableHttpReload){ + setTableHttpReload(true) + return Promise.resolve({ + data: tableListDataSource, + success: true, + }); + } + const query = { + page:params.current, + pageSize:params.pageSize, + keyword:searchWord, + } + return fetchData>( + `dynamic/${moduleId}/list`, + {method:'GET',eoParams:query,eoTransformKeys:['pageSize']}).then((res)=>{ + message.destroy(); + if(res.code === STATUS_CODE.SUCCESS){ + getConfig(res.data) + setColumns(res.data.basic.fields.map((field:DynamicTableField)=>({ + title:field.title, + dataIndex:field.name, + copyable: true, + fixed:field.name === 'title' ? 'left' : undefined, + ellipsis:true, + width:field.name === 'title' ? 150 : undefined, + ...(field.enum?.length > 0 ?{ + onFilter: (value: string, record: { [x: string]: string | string[]; }) => record[field.name].indexOf(value) === 0, + filters:field.enum?.map((x:string)=>{return {text:x, value:x}}), + render:(_: unknown, entity: { [x: string]: string; })=> { + return {(entity[field.name] as string)} + }, + }:{}), + }))) + setTableListDataSource(res.data.list); + return ({ data: res.data.list, success: true,total:res.data.total }); + }else{ + setTableListDataSource([]); + return ({ data: [], success: false }); + } + }).catch((e)=>{console.warn(e); + return ({ data: [], success: false });}) + } + + const getConfig = (data:DynamicTableConfig)=>{ + const {basic,list } = data + const {title,drivers} = basic + + setBreadcrumb([ + {title:location.includes('resourcesettings') ? '资源配置': '日志配置'}, + { + title + } + ]) + + setPluginName(title) + setDriverOptions(drivers?.map((driver:DynamicDriverData) => { + return { label: driver.title, value: driver.name } + }) || []) + + } + + const getRender = ()=>{ + return fetchData>(`dynamic/${moduleId}/render`,{method:'GET'}).then((resp) => { + if (resp.code === STATUS_CODE.SUCCESS) { + setRenderSchema(resp.data.render) + return Promise.resolve(resp.data.render) + } + return Promise.reject(resp.msg || '操作失败') + }) + } + + const operation:ProColumns[] =[ + { + title: '操作', + key: 'option', + width: 150, + fixed:'right', + valueType: 'option', + render: (_: React.ReactNode, entity: DynamicTableItem) => [ + {openModal('publish',entity)}} btnTitle={entity.status === '已发布' ? '下线' : '上线'}/>, + , + {openDrawer('edit',entity)}} btnTitle="查看"/>, + , + {openModal('delete',entity)}} btnTitle="删除"/>, + ], + } + ] + const handleClusterChange = (e:string[])=>{ + setTableHttpReload(true) + pageListRef.current?.reload() + } + + const manualReloadTable = () => { + setTableHttpReload(true); // 表格数据需要从后端接口获取 + pageListRef.current?.reload() + }; + + const deleteInstance = (entity:DynamicTableItem)=>{ + return new Promise((resolve, reject)=>{ + fetchData>(`dynamic/${moduleId}/batch`,{method:'DELETE',eoParams:{ids:JSON.stringify([entity!.id])}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }) + }) + } + + const openDrawer = async (type:'add'|'edit', entity?:DynamicTableItem)=>{ + switch (type){ + case 'add': + setCurDetail({driver:driverOptions[0].value || '',config:{'c3ebd745-f7d5-45cd-8d3e-e0e43099d20e':{scopes:[]},'550e2537-8436-48e4-ab84-f9f58faf1b18':{scopes:[]}}}) + break; + case 'edit':{ + setDrawerLoading(true) + fetchData>( + `dynamic/${moduleId}/info`, + {method:'GET',eoParams:{id:entity!.id}}).then((res)=>{ + const {code, data, msg } = res + if(code === STATUS_CODE.SUCCESS){ + if(data.info.config){ + for (const tab in data.info.config) { + data.info.config[tab]._apinto_show = true + } + } + setCurDetail(data.info) + }else{ + message.error(msg || '操作失败') + } + }).finally(()=>setDrawerLoading(false)) + break; + } + } + setDrawerType(type) + setDrawerOpen(true) + } + + const openModal = async (type:'publish'|'delete', entity?:DynamicTableItem)=>{ + let title:string = '' + let content:string|React.ReactNode = '' + switch (type){ + case 'publish':{ + message.loading('正在操作') + await fetchData>(`dynamic/${moduleId}/${entity!.status === '已发布' ? 'offline':'online'}`, { + method: 'PUT', + eoParams:{id:entity!.id}, + }).then(response => { + const {code, msg} = response + if (code === STATUS_CODE.SUCCESS) { + message.success(msg || '操作成功!') + return Promise.resolve(true) + } else { + message.error(msg || '操作失败') + return Promise.reject(msg || '操作失败') + } + }).catch((errorInfo)=> Promise.reject(errorInfo)) + message.destroy() + return;} + case 'delete': + title='删除' + content=确定删除成员?此操作无法恢复,确认操作? + break; + } + + modal.confirm({ + title, + content, + onOk:()=>{ + switch (type){ // case 'publish': + // return editRef.current?.save().then((res)=>{if(res === true) manualReloadTable()}) + case 'delete': + return deleteInstance(entity!).then((res)=>{if(res === true) manualReloadTable()}) + } + }, + width: type === 'delete'? 600 : 900, + okText:'确认', + okButtonProps:{ + disabled:false + }, + cancelText:'取消', + closable:true, + icon:<>, + footer:(_, { OkBtn, CancelBtn }) =>{ + return ( + <> + + + + ); + }, + }) + } + + useEffect(() => { + getRender() + pageListRef.current?.reload() + }, [moduleId]); + + + return (<> + getIntelligentPluginTableList(params)} + addNewBtnTitle={`添加${pluginName}`} + searchPlaceholder={`搜索${pluginName}名称`} + onChange={() => { + setTableHttpReload(false) + }} + addNewBtnAccess={`${accessPrefix}.add`} + onAddNewBtnClick={()=>{openDrawer('add')}} + onSearchWordChange={(e)=>{setSearchWord(e.target.value);setTableHttpReload(true);setTableHttpReload(true)}} + /> + + {setCurDetail(undefined);setDrawerOpen(false)}} onSubmit={()=>drawerFormRef.current?.save()?.then((res)=>{res && manualReloadTable();return res})} submitAccess=''> + } spinning={drawerLoading}> + + + + ) +} \ No newline at end of file diff --git a/frontend/packages/common/src/components/apispace/code-snippet/code-example.type.ts b/frontend/packages/common/src/components/apispace/code-snippet/code-example.type.ts new file mode 100644 index 00000000..79cf9010 --- /dev/null +++ b/frontend/packages/common/src/components/apispace/code-snippet/code-example.type.ts @@ -0,0 +1,52 @@ +export const DOMAIN_SUFIX = [ + 'com', + 'cn', + 'xin', + 'net', + 'top', + 'xyz', + 'wang', + 'shop', + 'site', + 'club', + 'cc', + 'fun', + 'online', + 'biz', + 'red', + 'link', + 'ltd', + 'mobi', + 'info', + 'org', + 'name', + 'vip', + 'pro', + 'work', + 'tv', + 'kim', + 'group', + 'tech', + 'store', + 'ren', + 'ink', + 'pub', + 'live', + 'wiki', + 'design', + 'ai', + 'me', + 'io', + 'test', + 'example', + 'invalid', + 'localhost' +] +export const HTTP_REQUEST_METHOD_ARR = ['POST', 'GET', 'PUT', 'DELETE', 'HEAD', 'OPTIONS', 'PATCH'] +export const JWT_METHOD: { + [key: string]: string +} = { + HS256: 'HmacSHA256', + HS384: 'HmacSHA384', + HS512: 'HmacSHA512' +} diff --git a/frontend/packages/common/src/components/apispace/code-snippet/code-snippets.type.ts b/frontend/packages/common/src/components/apispace/code-snippet/code-snippets.type.ts new file mode 100644 index 00000000..6c5f1c17 --- /dev/null +++ b/frontend/packages/common/src/components/apispace/code-snippet/code-snippets.type.ts @@ -0,0 +1,164 @@ +export type PARAM_TYPE = + | 'string' + | 'float' + | 'integer' + | 'boolean' + | 'date' + | 'time' + | 'datatime' + | string +export type PARAM_KEY_REF_TYPE = { + key: string + type: string + childKey?: string + value: string + attribute?: string + description?: string + filter?: string + arrayItemKey?:string +} +export type PARAM_TYPE_REF_TYPE = { + [key: string | number]: PARAM_TYPE +} +export type PARAM_LIS_ITEM_TYPE = { + [key: string]: unknown +} +export type PARAM_LIST_TYPE = PARAM_LIS_ITEM_TYPE[] +export type PARSE_PARAM_TYPE = 'json' | 'xml' | 'query' | 'formData' | 'header' + +export type CODE_LANGUAGE_SNIPPETS_TYPE = { + value: number + label: string + children?: CODE_LANGUAGE_SNIPPETS_TYPE[] + isLeaf?: boolean +} +export const CODE_SNIPPETS: CODE_LANGUAGE_SNIPPETS_TYPE[] = [ + { + label: 'Java(OK HTTP)', + value: 20, + isLeaf: true + }, + { + label: 'PHP', + value: 9, + children: [ + { + label: 'pecl_http', + value: 10, + isLeaf: true + }, + { + label: 'cURL', + value: 11, + isLeaf: true + } + ] + }, + { + label: 'Python', + value: 12, + children: [ + { + label: 'http.client(Python 3)', + value: 13, + isLeaf: true + }, + { + label: 'Requests', + value: 14, + isLeaf: true + } + ] + }, + { + label: 'HTTP', + value: 1, + isLeaf: true + }, + { + label: 'cURL', + value: 2, + isLeaf: true + }, + { + label: 'JavaScript', + value: 3, + children: [ + { + label: 'Jquery AJAX', + value: 4, + isLeaf: true + }, + { + label: 'XHR', + value: 5, + isLeaf: true + } + ] + }, + { + label: 'NodeJS', + value: 6, + children: [ + { + label: 'Native', + value: 7, + isLeaf: true + }, + { + label: 'Request', + value: 8, + isLeaf: true + } + ] + }, + { + label: '微信小程序', + value: 21, + isLeaf: true + }, + { + label: 'Ruby(Net:Http)', + value: 15, + isLeaf: true + }, + { + label: 'Shell', + value: 16, + children: [ + { + label: 'Httpie', + value: 17, + isLeaf: true + }, + { + label: 'cUrl', + value: 18, + isLeaf: true + } + ] + }, + { + label: 'Go', + value: 19, + isLeaf: true + } +] + +export const paramsJsonType: unknown = { + STRING: 'string', + FILE: 'file', + JSON: 'json', + INT: 'int', + FLOAT: 'float', + DATE: 'date', + DATETIME: 'datetime', + BOOLEAN: 'boolean', + BYTE: 'byte', + SHORT: 'short', + LONG: 'long', + ARRAY: 'array', + OBJECT: 'object', + NUMBER: 'number', + NULL: 'null' +} diff --git a/frontend/packages/common/src/components/apispace/code-snippet/generate-code.ts b/frontend/packages/common/src/components/apispace/code-snippet/generate-code.ts new file mode 100644 index 00000000..777aabc5 --- /dev/null +++ b/frontend/packages/common/src/components/apispace/code-snippet/generate-code.ts @@ -0,0 +1,1250 @@ +import { cloneDeep } from 'lodash-es' +import { parseFormData, parseFileValue, parseFileType, parseHeaders, parseRequestBodyToString, parseUri, payloadStr, goCodeParseFormData } from './transform' +// import { getJson } from '../.@common/utils/'; +import { ApiBodyType } from '@common/const/api-detail'; + +function sameNameToParams(params: unknown) { + params = cloneDeep(params) + const output: unknown = [] + const keyMUI: unknown = [] + params.forEach((val: unknown) => { + const paramIndex = keyMUI.indexOf(val.name) + if (paramIndex == -1) { + output.push(val) + keyMUI.push(val.name) + } else if (Object.prototype.toString.call(output[paramIndex].value) == '[object Array]') { + output[paramIndex].value.push(val.value || '') + } else { + output[paramIndex].value = [output[paramIndex].value, val.value] + } + }) + return output +} +function stringifyParams(val: unknown) { + val.name = JSON.stringify(val.name) + val.value = JSON.stringify(val.example || '') + return val +} +function stringifyHeaders(val: unknown) { + val.name = JSON.stringify(val.name) + val.value = JSON.stringify(val.value || '') + return val +} +function encodeURIComponentParams(val: unknown) { + val.name = encodeURIComponent(val.name) + val.value = encodeURIComponent(val.example || '') + return val +} +function enrichParams(params: unknown) { + const result = cloneDeep(params) + result.forEach((val: unknown) => { + val.valueQuery = [] + let defaultIndex = 0 + val.value_list?.forEach((child: unknown, index: number) => { + if (child.is_default) defaultIndex = index + //@ts-ignore + val.valueQuery.push(child.value) + }) + val.value = val.example || val.valueQuery[defaultIndex] || val.value || '' + val.value = val.value === null ? '' : val.value || '' + }) + return result +} +export function generateCode( + type: string, + multipart: boolean, + { protocol, URL, headers, params, method, requestType, apiRequestParamJsonType, raw }: unknown, +) { + requestType=ApiBodyType[requestType] + let code: string = '' + const indent = ' ' + let urlObj: unknown = {} + try { + urlObj = new window.URL(parseUri(protocol, decodeURIComponent(URL))) + } catch (URL_PARSE_ERROR) { + urlObj = { + protocol: 'http', + href: URL, + pathname: '', + search: '', + host: '', + searchParams: [], + hostname: '' + } + } + + const requestParam: string = parseRequestBodyToString({ + requestType, + params, + apiRequestParamJsonType, + raw + }) + + const langTmp: unknown = { + headerStr: '', + paramsStr: '' + } + params = enrichParams(params) + headers = enrichParams(headers) + + switch (type) { + // HTTP + case '1': { + let nullHost = false + langTmp.headerStr = parseHeaders(headers, { + format: '${name}:${value}\r\n' + }) + switch (requestType || 'FORMDATA') { + case 'FORMDATA': { + if (multipart) { + langTmp.paramsStr = parseFormData(params, { + langType: 'HTTP', + map: encodeURIComponentParams + }) + } else { + langTmp.paramsStr = parseFormData(params, { + format: '${name}=${value}', + separator: '&', + map: encodeURIComponentParams + }) + } + break + } + default: { + langTmp.paramsStr = requestParam || '' + } + } + if (URL === `/${urlObj.host}${urlObj.pathname}` || URL === `/${urlObj.host}`) { + nullHost = true + } + if (multipart) { + code = + `${method} ${nullHost ? `/${urlObj.host}${urlObj.pathname}` : urlObj.pathname || ''}${ + urlObj.search || '' + } HTTP/1.1\r\n` + + `Host: ${nullHost ? '' : urlObj.host || ''}\r\n` + + `${langTmp.headerStr}` + + 'Content-Length: 392\r\n' + + 'Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW\r\n\r\n' + + `${langTmp.paramsStr}\r\n` + } else { + code = + `${method} ${nullHost ? `/${urlObj.host}${urlObj.pathname}` : urlObj.pathname || ''}${ + urlObj.search || '' + } HTTP/1.1\r\n` + + `Host: ${nullHost ? '' : urlObj.host || ''}\r\n` + + `${langTmp.headerStr ? langTmp.headerStr + '\r\n' : ''}` + + `${langTmp.paramsStr}\r\n` + } + break + } + // cURL + case '2': { + let cookieStr = null + langTmp.headerStr = parseHeaders(headers, { + format: " -H '${name}:${value}'", + separator: ' \\\r\n', + filter(header: unknown) { + if (!multipart) return true + if ('content-type'.indexOf(header.name.toLowerCase()) === -1) { + return true + } else { + return false + } + } + }) + for (let i = 0; i < headers.length; i++) { + const { name } = headers[i] + if (name.toLowerCase() === 'cookie') { + cookieStr = ` -b ${headers[i].value}` + } + } + switch (requestType || 'FORMDATA') { + case 'FORMDATA': { + if (multipart) { + let tmpOutput: unknown = '' + params.map((val: unknown) => { + if (val.data_type === 'file') { + tmpOutput = ` --form '${val.name}=@"${val.value}"' \\` + } + // if (val.files) { + // const filesArr = val.value.split(',') + // if (val.files.length) { + // val.files.map((childVal: unknown, childKey: unknown) => { + // tmpOutput.push(` -F '${val.name}=@${filesArr[childKey]}'`) + // }) + // } + // } else { + // tmpOutput.push(` -F '${val.name}=${val.value}'`) + // } + }) + langTmp.paramsStr = tmpOutput + } else { + const tmpOutput: unknown = parseFormData(params, { + format: '${name}=${value}', + separator: '&', + map: encodeURIComponentParams + }) + langTmp.paramsStr = tmpOutput ? ` -d '${tmpOutput}'` : '' + } + break + } + default: { + langTmp.paramsStr = requestParam ? ` -d '${requestParam}'` : '' + } + } + code = + `curl -X ${method} \\\r\n` + + `${urlObj.href ? ` '${urlObj.href}' \\\r\n` : ''}` + + `${langTmp.headerStr}` + + `${cookieStr ? ` \\\r\n${cookieStr}` : ''}` + + `${langTmp.paramsStr ? ` \\\r\n${langTmp.paramsStr}` : ''}` + break + } + // JavaScript>Jquery AJAX + case '4': { + const langTmp: unknown = { + headerStr: '', + paramsStr: '' + } + langTmp.headerStr = parseHeaders(headers, { + format: `${indent + indent}\${name}:\${value}`, + separator: ',\r\n', + map: stringifyHeaders + }) + switch (requestType || 'FORMDATA') { + case 'FORMDATA': { + if (multipart) { + langTmp.paramsStr = `var data = new FormData();\r\n${parseFormData(params, { + format: 'data.append(${name},${value});', + separator: '\r\n', + map: stringifyParams + })}` + } else { + langTmp.paramsStr = `var data = {\n${parseFormData(params, { + format: `${indent}\${name}: \${value}`, + separator: ',\r\n', + init: sameNameToParams, + map: stringifyParams + })}\r\n}` + } + break + } + default: { + langTmp.paramsStr = `var data = ${JSON.stringify(requestParam)}` + } + } + code = + `${langTmp.paramsStr}\r\n\r\n` + + '$.ajax({\r\n' + + `${indent}"url":"${urlObj.href || ''}",\r\n` + + `${indent}"method": "${method}",\r\n` + + `${indent}"headers": {\r\n` + + `${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` + + `${indent}},\r\n` + + `${ + multipart ? `${indent}"processData": false,\r\n${indent}"contentType": false,\r\n` : '' + }` + + `${indent}"data": data,\r\n` + + `${indent}"crossDomain": true\r\n` + + '})\r\n' + + `${indent}.done(function(response){})\r\n` + + `${indent}.fail(function(jqXHR){})\r\n` + break + } + // JavaScript>XHR + case '5': { + langTmp.headerStr = parseHeaders(headers, { + format: 'xhr.setRequestHeader(${name},${value});', + separator: '\r\n', + map: stringifyHeaders + }) + switch (requestType || 'FORMDATA') { + case 'FORMDATA': { + if (multipart) { + langTmp.paramsStr = `var data = new FormData();\r\n${parseFormData(params, { + format: 'data.append(${name},${value});', + separator: '\r\n', + map: stringifyParams + })}` + } else { + langTmp.paramsStr = `var data = ${JSON.stringify( + parseFormData(params, { + format: '${name}=${value}', + separator: '&', + map: encodeURIComponentParams + }) + )};` + } + break + } + default: { + langTmp.paramsStr = `var data = ${JSON.stringify(requestParam)}` + } + } + code = + `${langTmp.paramsStr}\r\n\r\n` + + 'var xhr = new XMLHttpRequest();\r\n' + + 'xhr.withCredentials = false;\r\n\r\n' + + 'xhr.addEventListener("readystatechange", function () {\r\n' + + `${indent}if (this.readyState === 4) {\r\n` + + `${indent}${indent}console.log(this.responseText);\r\n` + + `${indent}}\r\n` + + '});\r\n\r\n' + + `xhr.open("${method}", "${urlObj.href || ''}");\r\n` + + `${langTmp.headerStr}\r\n\r\n` + + 'xhr.send(data);' + break + } + // NodeJS>Native + case '7': { + langTmp.headerStr = parseHeaders(headers, { + format: `${indent + indent}\${name}:\${value}`, + separator: ',\r\n', + map: stringifyHeaders + }) + switch (requestType || 'FORMDATA') { + case 'FORMDATA': { + if (multipart) { + langTmp.paramsStr = parseFormData(params, { + langType: 'NodeJSNative' + }) + } else { + langTmp.paramsStr = `qs.stringify({\n${parseFormData(params, { + format: `${indent + indent}\${name}: \${value}`, + separator: ',\r\n', + init: sameNameToParams, + map: stringifyParams + })}\r\n})` + } + break + } + default: { + langTmp.paramsStr = JSON.stringify(requestParam) + } + } + if (multipart) { + code = + 'const http = require("http");\r\n' + + 'const fs = require("fs");\r\n' + + 'const path = require("path");\r\n' + + 'const FormData = require("form-data"); \r\n\r\n' + + `${langTmp.paramsStr}\r\n\r\n` + + 'var requestInfo={\r\n' + + `${indent}"method": "${method}",\r\n` + + `${urlObj.hostname ? `${indent}"hostname": "${urlObj.hostname}",\r\n` : ''}` + + `${ + urlObj.port ? `${indent}"port": "${urlObj.port}",\r\n` : '' + }` + + `${ + urlObj.pathname || urlObj.search + ? `${indent}"path": "${urlObj.pathname || ''}${ + urlObj.search || '' + }",\r\n` + : '' + }` + + `${indent}"headers": {\r\n` + + `${langTmp.headerStr ? `${langTmp.headerStr},\r\n` : ''}` + + ' ...form.getHeaders()\r\n' + + ' }\r\n' + + '};\r\n\r\n' + + 'var req = http.request(requestInfo, function (res) {\r\n' + + `${indent}var chunks = [];\r\n\r\n` + + `${indent}res.on("data", function (chunk) {\r\n` + + `${indent}${indent}chunks.push(chunk);\r\n` + + `${indent}});\r\n\r\n` + + `${indent}res.on("end", function () {\r\n` + + `${indent}${indent}var body = Buffer.concat(chunks);\r\n` + + `${indent}${indent}console.log(body.toString());\r\n` + + `${indent}});\r\n` + + '});\r\n\r\n' + + 'form.pipe(req);' + } else { + code = + 'var qs = require("querystring");\r\n' + + `var http = require("${urlObj.protocol.replace(':', '')}");\r\n` + + 'var requestInfo={\r\n' + + `${indent}"method": "${method}",\r\n` + + `${ + urlObj.hostname + ? `${indent}"hostname": "${urlObj.hostname}",\r\n` + : '' + }` + + `${ + urlObj.port ? `${indent}"port": "${urlObj.port}",\r\n` : '' + }` + + `${ + urlObj.pathname || urlObj.search + ? `${indent}"path": "${urlObj.pathname || ''}${ + urlObj.search || '' + }",\r\n` + : '' + }` + + `${indent}"headers": {\r\n` + + `${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` + + ' }\r\n' + + '};\r\n\r\n' + + 'var req = http.request(requestInfo, function (res) {\r\n' + + `${indent}var chunks = [];\r\n\r\n` + + `${indent}res.on("data", function (chunk) {\r\n` + + `${indent}${indent}chunks.push(chunk);\r\n` + + `${indent}});\r\n\r\n` + + `${indent}res.on("end", function () {\r\n` + + `${indent}${indent}var body = Buffer.concat(chunks);\r\n` + + `${indent}${indent}console.log(body.toString());\r\n` + + `${indent}});\r\n` + + '});\r\n\r\n' + + `req.write(${langTmp.paramsStr});\r\n` + + 'req.end();' + } + break + } + // NodeJS>Request + case '8': { + langTmp.headerStr = parseHeaders(headers, { + format: ' ${name}:${value}', + separator: ',\r\n', + map: stringifyHeaders + }) + switch (requestType || 'FORMDATA') { + case 'FORMDATA': { + if (multipart) { + langTmp.paramsStr = ` formData: {\r\n${parseFormData(params, { + format: ' ${name}: ${value}', + separator: ',\r\n', + map(val: unknown) { + if (val.files && val.files.length) { + const fileNameArr = val.value.split(',') + val.value = `{value:fs.createReadStream(${JSON.stringify(fileNameArr[0])})}` + } else { + val.value = JSON.stringify(val.value) + } + val.name = JSON.stringify(val.name) + return val + } + })}\r\n }` + } else { + langTmp.paramsStr = ` form: {\n${parseFormData(params, { + format: ' ${name}: ${value}', + separator: ',\r\n', + init: sameNameToParams, + map: stringifyParams + })}\r\n }` + } + break + } + default: { + langTmp.paramsStr = ` body: ${JSON.stringify(requestParam)}` + } + } + code = + `${ + (requestType || 'FORMDATA') === 'FORMDATA' && multipart + ? 'var fs = require("fs");\r\n' + : '' + }` + + 'var request = require("request");\r\n' + + 'var requestInfo={\r\n' + + ` method: "${method}",\r\n` + + ` url: "${urlObj.href}",\r\n` + + ' headers: {\r\n' + + `${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` + + ' },\r\n' + + `${langTmp.paramsStr}` + + '\r\n};\r\n\r\n' + + 'request(requestInfo, function (error, response, body) {\r\n' + + ' if (error) throw new Error(error);\r\n' + + ' console.log(body);\r\n' + + '});' + break + } + // PHP>pecl_http + case '10': { + langTmp.headerStr = parseHeaders(headers, { + format: ' ${name} => ${value}', + separator: ',\r\n', + map: stringifyHeaders + }) + switch (requestType || 'FORMDATA') { + case 'FORMDATA': { + if (multipart) { + params = sameNameToParams(params) + let tmpOutput: unknown = '' + params.map((val: unknown, key: number) => { + if(val.data_type === 'file') { + tmpOutput += + ` "${val.name}" =>array(\r\n` + + ` "type" => "${parseFileType(val.value)}",\r\n` + + ' "content" => fopen($file_path, "r"),\r\n' + + ' "name" => $file_name,\r\n' + + ` )${key === params.length - 1 ? '' : ','}\r\n` + } else { + tmpOutput += ` ${JSON.stringify(val.name)} => ${JSON.stringify(val.value)}\r` + + } + }) + langTmp.paramsStr = + 'addForm(' + `${tmpOutput.length ? `array(\r\n${tmpOutput})` : ''}` + '\r\n);' + langTmp.fileValue = parseFileValue(params) + } else { + langTmp.paramsStr = `append(new http\\QueryString(array({\r\n${parseFormData(params, { + format: ' ${name} => ${value}', + separator: ',\r\n', + init: sameNameToParams, + map(val: unknown) { + val.name = JSON.stringify(val.name) + if (Object.prototype.toString.call(val.value) == '[object Array]') { + const InfoStr: unknown = [] + val.value.forEach((val: unknown) => { + InfoStr.push(` ${val}`) + }) + val.value = `array(\r\n${InfoStr.join(',\r\n')} \r\n )` + } else { + val.value = JSON.stringify(val.value) + } + return val + } + })}\r\n))));` + } + break + } + default: { + langTmp.paramsStr = `append(${JSON.stringify(requestParam)});` + } + } + const tmpOutput: unknown = [] + urlObj.searchParams.forEach((val: unknown, key: unknown) => { + tmpOutput.push(` ${JSON.stringify(key)} => ${JSON.stringify(val)}`) + langTmp.queryStr = `$request->setQuery(new http\\QueryString(array(\r\n${tmpOutput.join( + ',\r\n' + )}\r\n)));` + }) + if (multipart) { + code = + 'setRequestUrl("${urlObj.host + urlObj.pathname}");\r\n` + + `$request->setRequestMethod("${method}");\r\n` + + '$request->setBody($body);\r\n\r\n' + + `$request->getBody()->${langTmp.paramsStr}\r\n\r\n` + + '$request->setHeaders(array(\r\n' + + `${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` + + ' "Content-Type":"multipart/form-data"\r\n' + + '));\r\n\r\n' + + '$client->enqueue($request)->send();\r\n' + + '$response = $client->getResponse();\r\n\r\n' + + 'echo $response->getBody();\r\n' + } else { + code = + '${langTmp.paramsStr}\n\r` + + `$request->setRequestUrl("${urlObj.host + urlObj.pathname}");\r\n` + + `$request->setRequestMethod("${method}");\r\n` + + '$request->setBody($body);\r\n\r\n' + + `${langTmp.queryStr ? `${langTmp.queryStr}\r\n\r\n` : ''}` + + '$request->setHeaders(array(\r\n' + + `${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` + + '));\r\n\r\n' + + '$client->enqueue($request)->send();\r\n' + + '$response = $client->getResponse();\r\n\r\n' + + 'echo $response->getBody();\r\n' + } + break + } + // PHP>cURL + case '11': { + langTmp.headerStr = parseHeaders(headers, { + format: ' "${name}:${value}"', + separator: ',\r\n' + }) + switch (requestType || 'FORMDATA') { + case 'FORMDATA': { + if (multipart) { + let tmpOutput = '' + params.map((val: unknown, key: number) => { + if (val.data_type === 'file') { + tmpOutput += ` "${val.name}" => new CURLFile($file_path)${ + key === params.length - 1 ? '' : ',' + }\r\n` + } else { + tmpOutput += ` ${JSON.stringify(val.name)} => ${JSON.stringify(val.value)}\r` + } + }) + langTmp.paramsStr = `${tmpOutput.length ? `array(\r\n${tmpOutput} )` : ''}` + langTmp.fileValue = parseFileValue(params) + langTmp.paramsStr = JSON.stringify(parseFormData(params)) + } else { + langTmp.paramsStr = JSON.stringify( + parseFormData(params, { + format: '${name}=${value}', + separator: '&', + map: encodeURIComponentParams + }) + ) + } + break + } + default: { + langTmp.paramsStr = JSON.stringify(requestParam) + } + } + if (multipart) { + code = + ' "${urlObj.port}",\r\n` : ''}` + + ` CURLOPT_URL => "${urlObj.href}",\r\n` + + ' CURLOPT_RETURNTRANSFER => true,\r\n' + + ' CURLOPT_ENCODING => "",\r\n' + + ' CURLOPT_MAXREDIRS => 10,\r\n' + + ' CURLOPT_TIMEOUT => 30,\r\n' + + ' CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,\r\n' + + ` CURLOPT_CUSTOMREQUEST => "${method}",\r\n` + + ` CURLOPT_POSTFIELDS => ${langTmp.paramsStr},\r\n` + + ' CURLOPT_HTTPHEADER => array(\r\n' + + `${langTmp.headerStr ? `${langTmp.headerStr},\r\n` : ''}` + + ' "Content-Type:multipart/form-data"' + + '\r\n ),\r\n' + + '));\r\n\r\n' + + '$response = curl_exec($curl);\r\n\r\n' + + '$err = curl_error($curl);\r\n\r\n' + + 'curl_close($curl);\r\n\r\n' + + 'if ($err) {\r\n' + + ' echo "cURL Error #:" . $err;\r\n' + + '} else {\r\n' + + ' echo $response;\r\n' + + '}\r\n' + } else { + code = + ' "${urlObj.port}",\r\n` : ''}` + + ` CURLOPT_URL => "${urlObj.href}",\r\n` + + ' CURLOPT_RETURNTRANSFER => true,\r\n' + + ' CURLOPT_ENCODING => "",\r\n' + + ' CURLOPT_MAXREDIRS => 10,\r\n' + + ' CURLOPT_TIMEOUT => 30,\r\n' + + ' CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,\r\n' + + ` CURLOPT_CUSTOMREQUEST => "${method}",\r\n` + + ` CURLOPT_POSTFIELDS => ${langTmp.paramsStr},\r\n` + + ` CURLOPT_HTTPHEADER => array(\r\n${langTmp.headerStr}` + + '\r\n ),\r\n' + + '));\r\n\r\n' + + '$response = curl_exec($curl);\r\n\r\n' + + '$err = curl_error($curl);\r\n\r\n' + + 'curl_close($curl);\r\n\r\n' + + 'if ($err) {\r\n' + + ' echo "cURL Error #:" . $err;\r\n' + + '} else {\r\n' + + ' echo $response;\r\n' + + '}\r\n' + } + break + } + // Python>http.client(Python 3) + case '13': { + langTmp.headerStr = parseHeaders(headers, { + format: ' ${name}:${value}', + separator: ',\r\n', + map: stringifyHeaders + }) + switch (requestType || 'FORMDATA') { + case 'FORMDATA': { + if (multipart) { + langTmp.paramsStr = parseFormData(params, { + hasFileParams: true, + format: `${indent}\${name}: \${value}`, + separator: ',\r\n', + init: sameNameToParams, + map: stringifyParams + }) + langTmp.fileValue = parseFileValue(params) + } else { + langTmp.paramsStr = JSON.stringify( + parseFormData(params, { + format: '${name}=${value}', + separator: '&', + map: encodeURIComponentParams + }) + ) + } + break + } + case 'JSON': { + langTmp.paramsStr = requestParam + break + } + default: { + langTmp.paramsStr = JSON.stringify(requestParam) + } + } + if (multipart) { + code = + 'import http.client\r\n' + + 'import mimetypes\r\n' + + 'from requests_toolbelt.multipart.encoder import MultipartEncoder\r\n\r\n' + + `conn = http.client.HTTPSConnection("${urlObj.host}")\r\n` + + `headers = {\r\n${langTmp.headerStr}\r\n` + + '}\r\n\r\n' + + 'payload = { \r\n' + + `${langTmp.paramsStr}\r\n` + + '} \r\n' + + 'encoder = MultipartEncoder(payload)\r\n' + + 'headers["Content-Type"] = encoder.content_type\r\n' + + 'print(encoder.content_type)\r\n' + + `conn.request("${method}",${`"${urlObj.pathname}${urlObj.search}"`}, body=encoder.to_string(), headers=headers)\r\n` + + 'res = conn.getresponse()\r\n' + + 'data = res.read()\r\n' + + 'print(data.decode("utf-8"))\r\n' + } else { + code = + 'import http.client\r\n\r\n' + + `conn = http.client.HTTPSConnection("${urlObj.host}")\r\n\r\n` + + `payload = ${langTmp.paramsStr}\r\n\r\n` + + `headers = {\r\n${langTmp.headerStr}` + + '\r\n}\r\n\r\n' + + `conn.request("${method}",${`"${urlObj.pathname}${urlObj.search}"`}, payload, headers)\r\n\r\n` + + 'res = conn.getresponse()\r\n\r\n' + + 'data = res.read()\r\n\r\n' + + 'print(data.decode("utf-8"))\r\n' + } + break + } + // Python>Requests + case '14': { + langTmp.headerStr = parseHeaders(headers, { + format: ' ${name}:${value}', + separator: ',\r\n', + map: stringifyHeaders + }) + switch (requestType || 'FORMDATA') { + case 'FORMDATA': { + if (multipart) { + langTmp.paramsStr = parseFormData(params, { + format: `${indent}\${name}: \${value}`, + separator: ',\r\n', + init: sameNameToParams, + map: stringifyParams + }) + langTmp.fileValue = parseFileValue(params) + } else { + langTmp.paramsStr = JSON.stringify( + parseFormData(params, { + format: '${name}=${value}', + separator: '&', + map: encodeURIComponentParams + }) + ) + // langTmp.paramsStr = JSON.stringify( + // getJson(params, { + // ignoreCheckbox: true + // }) + // ) + } + break + } + case 'JSON': { + langTmp.paramsStr = requestParam + break + } + default: { + langTmp.paramsStr = JSON.stringify(requestParam) + } + } + const tmpOutput: unknown = [] + urlObj.searchParams.forEach((val: unknown, key: unknown) => { + tmpOutput.push(`${JSON.stringify(key)} : ${JSON.stringify(val)}`) + // langTmp.querystring = `querystring={${tmpOutput.join(',')}};` + langTmp.querystring = `{${tmpOutput.join(',')}}` + + }) + if(multipart) { + code = + 'import requests \r\n\r\n' + + 'headers = {\r\n' + + `${langTmp.headerStr}\r\n` + + '}\r\n' + + `url = "${method === 'GET' ? urlObj.origin + urlObj.pathname : urlObj.href}"\r\n` + + '//获取文件,需填路径 \r\n' + + "file_path = '' \r\n" + + `filename = "${langTmp.fileValue}" \r\n` + + `filetype = "${langTmp.fileType}" \r\n` + + 'data = { \r\n' + + `${langTmp.paramsStr}\r\n` + + '} \r\n' + + 'files = {"file": (filename, open(file_path, "rb"), filetype)} \r\n' + + `response=requests.${method.toLowerCase()}(url, files=files, headers=headers, data=data)\r\n` + + 'print(response.text)\r\n' + } else { + code = + 'import requests\r\n\r\n' + + `url = "${method === 'GET' ? urlObj.origin + urlObj.pathname : urlObj.href}"\r\n\r\n` + + `payload = ${ + langTmp.querystring ? langTmp.querystring : langTmp.paramsStr + }\r\n\r\n` + + `headers = {\r\n${langTmp.headerStr}` + + '\r\n}\r\n\r\n' + + `response=requests.request("${method}", url, ${payloadStr(method, headers)}, headers=headers)\r\n\r\n` + + 'print(response.text)\r\n' + } + break + } + // Ruby(Net:Http) + case '15': { + langTmp.headerStr = parseHeaders(headers, { + format: 'request[${name}] = ${value}', + separator: '\r\n', + map: stringifyHeaders + }) + switch (requestType || 'FORMDATA') { + case 'FORMDATA': { + if (multipart) { + let tmpOutput = '' + params.map((val: unknown, key: number) => { + if (val.data_type === 'file') { + tmpOutput += ` "${val.name}" => new CURLFile($file_path)${ + key === params.length - 1 ? '' : ',' + }\r\n` + } else { + tmpOutput += ` ${JSON.stringify(val.name)} => ${JSON.stringify(val.value)}\r` + } + }) + langTmp.paramsStr = `${tmpOutput.length ? `array(\r\n${tmpOutput} )` : ''}` + langTmp.fileValue = parseFileValue(params) + } else { + langTmp.paramsStr = JSON.stringify( + parseFormData(params, { + format: '${name}=${value}', + separator: '&', + map: encodeURIComponentParams + }) + ) + } + break + } + default: { + langTmp.paramsStr = JSON.stringify(requestParam) + } + } + code = + "require 'uri'\r\n" + + "require 'net/http'\r\n\r\n" + + `url = URI("${urlObj.href}")\r\n\r\n` + + 'http = Net::HTTP.new(url.host, url.port)\r\n\r\n' + + `request = Net::HTTP::${method.toLowerCase().replace(/^\S/, (s: string) => { + return s?.toUpperCase() + })}.new(url)\r\n` + + `${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` + + `request.body = ${langTmp.paramsStr}\r\n\r\n` + + 'response = http.request(request)\r\n' + + 'puts response.read_body' + break + } + // Shell>Httpie + case '17': { + langTmp.headerStr = parseHeaders(headers, { + format: ' ${name}:${value}', + separator: ' \\\r\n' + }) + switch (requestType || 'FORMDATA') { + case 'FORMDATA': { + if (multipart) { + langTmp.paramsStr = parseFormData(params, { + langType: 'shellHttpie' + }) + // langTmp.paramsStr = `echo '${parseFormData(params)}' | \\` + } else { + langTmp.paramsStr = parseFormData(params, { + format: ' ${name}=${value}', + separator: ' \\\r\n', + map(val: unknown) { + val.value = JSON.stringify(val.value) + return val + } + }) + } + break + } + default: { + langTmp.paramsStr = `${requestParam}` + } + } + code = + `${ + requestType === 'JSON' ? `printf '${langTmp.paramsStr}'|` : '' + } http ${ + (requestType || 'FORMDATA').toString() === 'FORMDATA' && + !multipart + ? '--form' + : (requestType || 'JSON').toString() === 'JSON' && + !multipart + ? '--follow' + : '' + } ${multipart ? '--ignore-stdin --form --follow' : ''} ${method} '${ + urlObj.href + }' ${multipart ? '\\\r' : '\\'}` + + `${multipart ? langTmp.paramsStr : ''}\r` + + `${langTmp.headerStr}` + + `${ + (requestType || 'FORMDATA').toString() === 'FORMDATA' && + !multipart && + langTmp.paramsStr + ? ` \\\r\n${langTmp.paramsStr}` + : '' + }` + break + } + // Shell>cUrl + case '18': { + langTmp.headerStr = parseHeaders(headers, { + format: " --header '${name}:${value}'", + separator: ' \\\r\n' + }) + switch (requestType || 'FORMDATA') { + case 'FORMDATA': { + if (multipart) { + let tmpOutput = '' + params.forEach((val: unknown, key: number) => { + if (val.data_type === 'file') { + tmpOutput = ` --form '${val.name}=@"${val.value}"' \\` + } + }) + langTmp.paramsStr = tmpOutput + + } else { + const tmpOutput = parseFormData(params, { + format: '${name}=${value}', + separator: '&', + map: encodeURIComponentParams + }) + langTmp.paramsStr = tmpOutput ? ` --data '${tmpOutput}'` : '' + } + break + } + default: { + langTmp.paramsStr = requestParam ? ` --data '${requestParam}'` : '' + } + } + code = + `curl --request ${method} \\\r\n` + + ` --url ${`'${urlObj.href}'` || ''} \\\r\n` + + `${langTmp.headerStr}` + + `${langTmp.paramsStr ? `\\\r\n${langTmp.paramsStr}` : ''}` + break + } + // Go + case '19': { + langTmp.headerStr = parseHeaders(headers, { + format: ' req.Header.Add(${name},${value})', + separator: '\r\n', + map: stringifyHeaders + }) + switch (requestType) { + case 'FORMDATA': { + if (multipart) { + langTmp.paramsStr = parseFormData(params, { langType: 'go' }) + code = + 'package main\r\n\r\n' + + 'import (\r\n' + + ' "bytes"\r\n' + + ' "fmt"\r\n' + + ' "io"\r\n' + + ' "mime/multipart"\r\n' + + ' "net/http"\r\n' + + ' "os"\r\n' + + ')\r\n\r\n' + + 'const ( \r\n' + + ' TextType = "text"\r\n' + + ' FileType = "file"\r\n' + + ')\r\n\r\n' + + 'type param struct { \r\n' + + ' key string\r\n' + + ' value string\r\n' + + ' typ string\r\n' + + '}\r\n\r\n' + + 'func main() {\r\n' + + ' body, err := request()\r\n' + + ' if err != nil {\r\n' + + ' fmt.Println(err)\r\n' + + ' return\r\n' + + ' }\r\n' + + ' fmt.Println(string(body))\r\n' + + '}\r\n\r\n' + + 'func writeFile(file string, writer io.Writer) error {\r\n' + + ' src, err := os.Open(file)\r\n' + + ' if err != nil {\r\n' + + ' return err\r\n' + + ' }\r\n' + + ' defer src.Close()\r\n' + + ' _, err = io.Copy(writer, src)\r\n' + + ' if err != nil {\r\n' + + ' return err\r\n' + + ' }\r\n' + + ' return nil\r\n' + + '}\r\n\r\n' + + 'func request() ([]byte, error) {\r\n' + + ' params := []*param{\r\n' + + `${langTmp.paramsStr}` + + ' }\r\n' + + ' body := new(bytes.Buffer)\r\n' + + ' writer := multipart.NewWriter(body)\r\n' + + ' for _, p := range params {\r\n' + + ' switch p.typ { \r\n' + + ' case TextType: \r\n' + + ' writer.WriteField(p.key, p.value)\r\n' + + ' case FileType: \r\n' + + ' part, err := writer.CreateFormFile(p.key, p.value)\r\n' + + ' if err != nil {\r\n' + + ' return nil, err\r\n' + + ' }\r\n' + + ' err = writeFile(p.value, part)\r\n' + + ' if err != nil {\r\n' + + ' return nil, err\r\n' + + ' }\r\n' + + ' }\r\n' + + ' }\r\n\r\n' + + ' err := writer.Close()\r\n' + + ' if err != nil {\r\n' + + ' return nil, err\r\n' + + ' }\r\n' + + ` req, err := http.NewRequest("${method}","${urlObj.href}", body)\r\n\r\n` + + ' if err != nil {\r\n' + + ' return nil, err\r\n' + + ' }\r\n' + + `${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` + + ' req.Header.Add("Content-Type", writer.FormDataContentType())\r\n\r\n' + + ' client := &http.Client{}\r\n' + + ' resp, err := client.Do(req)\r\n' + + ' if err != nil {\r\n' + + ' return nil, err\r\n' + + ' }\r\n' + + ' defer resp.Body.Close()\r\n' + + ' return io.ReadAll(resp.Body)\r\n' + + '}' + } else { + langTmp.paramsStr = goCodeParseFormData(params) + code = + 'package main\r\n\r\n' + + 'import (\r\n' + + ' "fmt"\r\n' + + ' "io/ioutil"\r\n' + + ' "net/http"\r\n' + + ' "net/url"\r\n' + + ' "strings"\r\n' + + ')\r\n\r\n' + + 'func main() {\r\n' + + ' body, err := request()\r\n' + + ' if err != nil {\r\n' + + ' fmt.Println(err)\r\n' + + ' return\r\n' + + ' }\r\n' + + ' fmt.Println(string(body))\r\n' + + '}\r\n\r\n' + + 'func request() ([]byte, error) {\r\n' + + ` uri := "${urlObj.href}"\r\n\r\n` + + ' payload := url.Values{}\r\n' + + ` ${langTmp.paramsStr ? ` ${langTmp.paramsStr}` : ''}` + + `req, _ := http.NewRequest("${method}", uri, strings.NewReader(payload.Encode()))\r\n\r\n` + + `${langTmp.headerStr}\r\n\r\n` + + ' res, err := http.DefaultClient.Do(req)\r\n' + + ' if err != nil {\r\n' + + ' return nil, err\r\n' + + ' }\r\n' + + ' defer res.Body.Close()\r\n' + + ' return ioutil.ReadAll(res.Body)\r\n' + + '}' + } + break + } + default: { + langTmp.paramsStr = requestParam ? JSON.stringify(requestParam) : '' + code = + 'package main\r\n\r\n' + + 'import (\r\n' + + ' "bytes"\r\n' + + ' "encoding/json"\r\n' + + ' "fmt"\r\n' + + ' "io/ioutil"\r\n' + + ' "net/http"\r\n' + + ')\r\n\r\n' + + 'func main() {\r\n' + + ' body, err := request()\r\n' + + ' if err != nil {\r\n' + + ' fmt.Println(err)\r\n' + + ' return\r\n' + + ' }\r\n' + + ' fmt.Println(string(body))\r\n' + + '}\r\n\r\n' + + 'func request() ([]byte, error) {\r\n' + + ` uri := "${urlObj.href}"\r\n\r\n` + + ` ${ + langTmp.paramsStr + ? ` payload := map[string]interface{}${langTmp.paramsStr}` + : ' payload := strings.NewReader("")' + }\r\n\r\n` + + ` req, _ := http.NewRequest("${method}", uri, bytes.NewBuffer(data))\r\n\r\n` + + `${langTmp.headerStr}\r\n\r\n` + + ' res, err := http.DefaultClient.Do(req)\r\n' + + ' if err != nil {\r\n' + + ' return nil, err\r\n' + + ' }\r\n' + + ' defer res.Body.Close()\r\n' + + ' return ioutil.ReadAll(res.Body)\r\n' + + '}' + } + } + break + } + // Java>OK HTTP + case '20': { + langTmp.headerStr = parseHeaders(headers, { + format: ' .addHeader(${name},${value})', + separator: '\r\n', + map: stringifyHeaders + }) + let mediaType = 'application/octet-stream' + switch ( (requestType || 'FORMDATA').toUpperCase()) { + case 'FORMDATA': { + if (multipart) { + mediaType = 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' + langTmp.paramsStr = parseFormData(params, { + langType: 'Java', + map: encodeURIComponentParams + }) + + langTmp.fileValue = parseFileValue(params) + langTmp.fileType = parseFileType(langTmp.fileValue) + } else { + mediaType = 'application/x-www-form-urlencoded' + langTmp.paramsStr = JSON.stringify( + parseFormData(params, { + format: '${name}=${value}', + separator: '&', + map: encodeURIComponentParams + }) + ) + } + break + } + case 'JSON': { + mediaType = 'application/json' + langTmp.paramsStr = JSON.stringify(requestParam) + break + } + case 'XML': { + mediaType = 'application/xml' + langTmp.paramsStr = JSON.stringify(requestParam) + break + } + default: { + langTmp.paramsStr = JSON.stringify(requestParam) + break + } + } + if(multipart) { + code = + 'OkHttpClient client = new OkHttpClient();\r\n\r\n' + + '//获取文件,需填路径 \r\n' + + 'File file = new File(""); \r\n\r\n' + + `RequestBody fileBody = RequestBody.create(MediaType.parse("${langTmp.fileType}"), file); \r\n\r\n` + + 'MultipartBody.Builder builder = new MultipartBody.Builder()\r\n' + + ' .setType(MultipartBody.FORM)\r\n' + + `${langTmp.paramsStr ? `${langTmp.paramsStr}\r\n` : ''}` + + 'Request request = new Request.Builder()\r\n' + + ` .url("${urlObj.href}")\r\n` + + `${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` + + ' .post(builder.build())\r\n' + + ' .build();\r\n\r\n' + + 'Response response = client.newCall(request).execute();\r\n' + + 'String result = response.body().string();\r\n' + + 'System.out.println(result);\r\n' + } else { + code = + 'OkHttpClient client = new OkHttpClient().newBuilder().build();\r\n' + + `MediaType mediaType = MediaType.parse("${mediaType}");\r\n` + + `${ + method === 'GET' + ? '' + : `RequestBody body = RequestBody.create(mediaType, ${langTmp.paramsStr});\r\n` + }` + + 'Request request = new Request.Builder()\r\n' + + ` .url("${urlObj.href}")\r\n` + + ` .method("${method}",${method === 'GET' ? 'null' : 'body'})\r\n` + + `${langTmp.headerStr ? `${langTmp.headerStr}\r\n` : ''}` + + ' .build();\r\n\r\n' + + 'Response response = client.newCall(request).execute();\r\n' + + 'System.out.println(response.body().string());\r\n' + } + + break + } + + // 微信小程序 + case '21': { + const langTmp: unknown = { + headerStr: '', + paramsStr: '' + } + langTmp.headerStr = parseHeaders(headers, { + format: `${indent + indent}\${name}:\${value}`, + separator: ',\r\n', + map: stringifyHeaders + }) + switch (requestType || 'FORMDATA') { + case 'FORMDATA': { + if (multipart) { + langTmp.paramsStr = `var data = new FormData();\r\n${parseFormData(params, { + format: 'data.append(${name},${value});', + separator: '\r\n', + map: stringifyParams + })}` + } else { + langTmp.paramsStr = `var data = {\n${parseFormData(params, { + format: `${indent}\${name}: \${value}`, + separator: ',\r\n', + init: sameNameToParams, + map: stringifyParams + })}\r\n}` + } + break + } + default: { + langTmp.paramsStr = `var data = ${JSON.stringify(requestParam)}` + } + } + code = + `${langTmp.paramsStr}\r\n\r\n` + + 'wx.request({\r\n' + + `${indent}"url":"${urlObj.href || ''}",\r\n` + + `${indent}"method": "${method}",\r\n` + + `${indent}"header": {\r\n` + + `${langTmp.headerStr}\r\n` + + `${indent}},\r\n` + + `${ + multipart ? `${indent}"processData": false,\r\n${indent}"contentType": false,\r\n` : '' + }` + + `${langTmp.paramsStr ? `${indent}"data": data,\r\n` : ''}` + + `${indent}"success": (response)=> {\r\n` + + `${indent + indent}console.log(response.data)\r\n` + + `${indent}}\r\n` + + '})\r\n' + break + } + default: { + break + } + } + return code +} diff --git a/frontend/packages/common/src/components/apispace/code-snippet/index.module.css b/frontend/packages/common/src/components/apispace/code-snippet/index.module.css new file mode 100644 index 00000000..db7fe109 --- /dev/null +++ b/frontend/packages/common/src/components/apispace/code-snippet/index.module.css @@ -0,0 +1,12 @@ + + +.code-snippet { + margin-bottom: 30px; + :global { + .ant-select-single.ant-select-sm .ant-select-selector { + font-size: 12px !important; + } + } +} + + diff --git a/frontend/packages/common/src/components/apispace/code-snippet/index.tsx b/frontend/packages/common/src/components/apispace/code-snippet/index.tsx new file mode 100644 index 00000000..b72cf4e3 --- /dev/null +++ b/frontend/packages/common/src/components/apispace/code-snippet/index.tsx @@ -0,0 +1,214 @@ +import { useEffect } from 'react'; +import { Cascader } from 'antd'; +import CODE_LANG from '@common/const/code/const'; +import type { DefaultOptionType } from 'antd/es/cascader'; +import { useState } from 'react'; +import { cloneDeep } from 'lodash-es'; +import { paramsJsonType } from './code-snippets.type'; +import { DOMAIN_SUFIX } from './code-example.type'; +import { transfromUrlParam } from './transform'; +import { generateCode } from './generate-code'; +import {ApiDetail} from "@common/const/api-detail"; +import {Codebox} from "@common/components/postcat//api/Codebox"; +import {Collapse} from "@common/components/postcat/api/Collapse"; +import {Box} from "@mui/material"; + +type CodeSnippetCompoType = { + title:string + api:ApiDetail, + extraTitle:unknown, + extraContent:unknown, + minLines:number +} + + const file: unknown[] = [] + const env: unknown = {} + const loading: boolean = false + const codeMode: string = 'rust' + const codeMens: string[] = ['reset', 'copy', 'download', 'newTab', 'search'] + const DOMAIN_REGEX: RegExp = new RegExp( + `^(((http|ftp|https):\/\/)|)(([\\\w\\\-_]+([\\\w\\\-\\\.]*)?(\\\.(${DOMAIN_SUFIX.join( + '|' + )})))|((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(localhost))((\\\/)|(\\\?)|(:)|($))` + ) + + let isMultipart: boolean = false + + export default function CodeSnippetCompo({title,api, extraTitle, extraContent, minLines=15}: CodeSnippetCompoType) { + // const [tokenState ] = useTokenBasicInfo() + const pretreatmentRequestInfo = (apiDoc: ApiDetail) =>{ + isMultipart = false + const result: ApiDetail = cloneDeep(apiDoc) + const files: unknown = file || [] + let isMuti: boolean = false + const headers: string[] = [] + let alreadyHadContentType: boolean = false + result.headers = [] + const originHeader = apiDoc.requestParams?.headerParams + //处理请求头部 + originHeader?.forEach((header: unknown) => { + // if ( + // tokenState?.selected_X_apibee_token && + // originHeader.length && + // originHeader[0].name == 'X-APISpace-Token' + // ) { + // originHeader[0].value = tokenState?.selected_X_apibee_token + // } + const { checkbox, name } = header + if ((checkbox || !header.hasOwnProperty('checkbox')) && name) { + headers.push(name?.toLowerCase()) + result.headers.push(header) + if (/content-type/i.test(name)) { + alreadyHadContentType = true + } + } + }) + const query: unknown = {} + + apiDoc.requestParams?.queryParams?.forEach((query: unknown) => { + const { checkbox, name } = query + if ((checkbox || !query.hasOwnProperty('checkbox')) && name) { + query[name] = query?.paramAttr.example || '' + } + }) + result.URL = transfromUrlParam(result.uri, query) + + //处理 restful 参数 + apiDoc.requestParams?.restParams?.forEach((rest: unknown) => { + if ((rest.checkbox || !rest.hasOwnProperty('checkbox')) && rest.name && rest.paramAttr.example) { + if (eval(`/:${rest.name}/`).test(result.URL.trim())) { + result.URL = result.URL.replaceAll(`:${rest.name}`, rest.paramAttr.example ) + } else if ( + result.URL.trim().indexOf(`{{${rest.name}}}`) == -1 && + result.URL.trim().indexOf(`{${rest.name}}`) > -1 + ) { + result.URL = result.URL.replaceAll(`{${rest.name}}`, rest.paramAttr.example) + } + } + }) + + result.params = [] + //为请求参数 中的header、reset、body、query 添加 value 和 valueQuery 的值 + switch (result.requestParams?.bodyParams?.[0]?.contentType) { + case 0: { + result.requestParams?.bodyParams?.forEach((body: unknown, key: unknown) => { + if ((body.checkbox || !body.hasOwnProperty('checkbox')) && body.name) { + if (paramsJsonType[body.dataType] == 'string' && body.paramAttr.example) { + isMuti = true + body.files = files[key] || [] + } + result.params.push(body) + } + }) + if (!alreadyHadContentType) { + if (isMuti) { + isMultipart = true + result.headers.push({ + name: 'Content-Type', + value: 'multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW', + checkbox: true + }) + } else { + result.headers.push({ + name: 'Content-Type', + value: 'application/x-www-form-urlencoded', + checkbox: true + }) + } + } + break + } + case 1: { + result.params = apiDoc.requestParams?.bodyParams + break + } + case 2: { + result.params = apiDoc.requestParams?.bodyParams + if (!alreadyHadContentType) { + result.headers.push({ + name: 'Content-Type', + value: 'application/json', + checkbox: true + }) + } + break + } + case 3: { + result.params = apiDoc.requestParams?.bodyParams + if (!alreadyHadContentType) { + result.headers.push({ + name: 'Content-Type', + value: 'application/xml', + checkbox: true + }) + } + break + } + } + + result.requestType = result.requestParams?.bodyParams?.[0]?.contentType || 0 + return result + } + + const [code, setCode] = useState('') + const [lang, setLang] = useState([20]) + + let tempCode = '' + const getCode = (language: number | string) => { + if (!['HTTPS', 'HTTP'].includes(api.protocol?.toUpperCase())) { + tempCode = '暂不支持生成非 HTTPS 或非 HTTP 协议的代码示例' + setCode(tempCode) + return + } + tempCode = generateCode( + language.toString(), + isMultipart, + pretreatmentRequestInfo(cloneDeep(api)) + ) + setCode(tempCode) + } + + useEffect(() => { + if(!Object.keys(api).length) return + getCode(lang[lang.length -1 ]) + }, [api]) + + + const onChange = (value: number[],record:DefaultOptionType[]) => { + const num = value[value.length - 1] + setLang(value) + if(!Object.keys(api).length) return + getCode(num) + }; + + const filter = (inputValue: string, path: DefaultOptionType[]) => + path.some( + (option) => (option.label as string).toLowerCase().indexOf(inputValue.toLowerCase()) > -1, + ); + + const [placeholderTxt, setPlaceholderTxt] = useState('搜索编程语言...') + const [selectItemTxt, setSelectItemTxt ] = useState('') + + return ( + + + + <> + 编程语言: onChange(value as unknown as number[],record)} + placeholder={placeholderTxt} + value={lang} // 当前的值 + showSearch={{ filter }} + size="small" + allowClear={false} + // onDropdownVisibleChange={value => openChange(value)} + />} + language={'javascript'} value={code} readOnly={true} height={'250px'} width={'100%'}/> + + + + + ) + + } \ No newline at end of file diff --git a/frontend/packages/common/src/components/apispace/code-snippet/transform.ts b/frontend/packages/common/src/components/apispace/code-snippet/transform.ts new file mode 100644 index 00000000..6069f895 --- /dev/null +++ b/frontend/packages/common/src/components/apispace/code-snippet/transform.ts @@ -0,0 +1,230 @@ +import { cloneDeep } from 'lodash-es' +import { PARAM_KEY_REF_TYPE, PARAM_TYPE_REF_TYPE } from './code-snippets.type'; +import { tranformJson, tranformXml } from './util'; +type LANG_TYPE = 'Java' | 'HTTP' | 'shellHttpie' | 'go' | 'NodeJSNative' +type PARAM_HEADER_TYPE = { name: string; value: string } +type PARSE_OPTS_TYPE = { + filter?: Function + map?: Function + init?: Function + format?: string + separator?: string + hasFileParams?: boolean + langType?: LANG_TYPE +} +export const paramsJsonType: unknown = { + STRING: 'string', + FILE: 'file', + JSON: 'json', + INT: 'int', + FLOAT: 'float', + DATE: 'date', + DATETIME: 'datetime', + BOOLEAN: 'boolean', + BYTE: 'byte', + SHORT: 'short', + LONG: 'long', + ARRAY: 'array', + OBJECT: 'object', + NUMBER: 'number', + NULL: 'null' +} + + + /** + * @description 拼接地址栏query参数,返回url + * @param {string} url 地址栏url + * @param {Object} query 地址栏query对象 + */ + export function transfromUrlParam(url: string, query: { [key: string]: string }) { + const querys: string[] = [] + Object.entries(query).forEach((item: string[]) => { + querys.push(`${item[0]}=${item[1]}`) + }) + if (!querys.length) return url + return `${url}${url.includes('?') ? '&' : '?'}${querys.join('&')}` + } + export function parseUri(protocol: string, url: string) { + if (!/((http:\/\/)|(https:\/\/))/.test(url)) { + url = (protocol == 'HTTPS' ? 'https://' : 'http://') + url + } + return url + } + + /** + * @description 处理FormData格式请求参数 + * @param {Object} options {format:生成Formdata格式[option],separator:组合字符串的分割符[option]} + * @param {Array} params 待拼接数组 + */ + export function parseFormData(params: unknown, { map, init, format, separator, langType, hasFileParams }: PARSE_OPTS_TYPE = {}) { + if (map) params = cloneDeep(params) + if (init) params = init(params) + if (format) { + //x-www + const result: unknown = [] + params.map((val: unknown) => { + if (map) val = map(val) + result.push(format.replace('${name}', val.name).replace('${value}', val.value)) + }) + return result.join(separator || '&') + } + //multipart + let result: string = '' + const boundary: string = 'WebKitFormBoundary7MA4YWxkTrZu0gW' + params.forEach((val: unknown) => { + if (map) val = map(val) + if (val.files) { + if (val.files.length) { + result += `------${boundary}\r\n` + val.files.map((childVal: unknown) => { + result += `content-disposition: form-data; name="${val.name}"; filename="${val.value}"\r\n` + if (typeof childVal === 'string') { + result += `Content-Type: ${((childVal.match(/data:(.*);/) || [])[0] || '') + .replace(/^data:/, '') + .replace(/;$/, '')}\r\n` + } else { + result += `Content-Type: ${(childVal.dataUrl.match(/data:(.*);/)[0] || '') + .replace(/^data:/, '') + .replace(/;$/, '')}\r\n` + } + result += '\r\n' + }) + } + } else { + result += `------${boundary}\r\n` + result += `content-disposition: form-data; name="${val.name}"\r\n` + result += '\r\n' + result += `${val.value}\r\n` + } + }) + result += `------${boundary}--` + return result + } + export function parseFileValue (params: unknown[]) { + const tmp = { + output: '' + } + if (params && params.length) { + for (let i = 0; i < params.length; i++) { + if (params[i].data_type === 'file') { + tmp.output = params[i].value || '' + break + } + } + return tmp.output + } + } + + + export function payloadStr (method: string, headers: unknown[]) { + let tmpStr = '' + if (method === 'GET') { + tmpStr = 'params=payload' + } else { + headers.forEach((item) => { + if (item.name === 'Content-Type') { + switch (item.description || item.value) { + case 'application/x-www-form-urlencoded': + tmpStr = 'data=payload' + break + case 'application/json': + tmpStr = 'data=json.dumps(payload)' + break + default: { + tmpStr = 'params=payload' + } + } + } + }) + } + return tmpStr + } + + export function goCodeParseFormData(params: unknown[]) { + if (!params.length) return + let output = '' + params.forEach((item) => { + output += `payload.Set("${item.name}", "${item.value}")\r\n ` + }) + return output + } + export function parseFileType (fileValue: string) { + const isPng = fileValue.endsWith('.png') + const isJpeg = fileValue.endsWith('.jpeg') + const isJpg = fileValue.endsWith('.jpg') + let result = 'image/jpeg' + if (isPng) { + result = 'image/png' + } else if (isJpg) { + result = 'image/jpeg' + } else if (isJpeg) { + result = 'image/jpeg' + } + return result + } + /** + * @description 处理请求头格式 + * @param {Object} options {format:生成请求头格式[option],separator:组合字符串的分割符[option]} + * @param {Array} headers 待拼接数组 + */ + export function parseHeaders( + headers: PARAM_HEADER_TYPE[], + { map, filter, format, separator }: PARSE_OPTS_TYPE = {} + ) { + const result = [] + if (map) { + headers = cloneDeep(headers) + } + for (const key in headers) { + let val = headers[key] + if (map) { + val = map(val) + } + if (filter) { + if (filter(val)) { + result.push(format?.replace('${name}', val.name)?.replace('${value}', val.value)) + } + } else { + result.push(format?.replace('${name}', val.name)?.replace('${value}', val.value)) + } + } + return result.join(separator || '') + } + const keyRefs: PARAM_KEY_REF_TYPE = { + key: 'name', + type: 'data_type', + value: 'value', + childKey: 'child_list', + arrayItemKey: 'isArrItem' + } + + const typeRefs: PARAM_TYPE_REF_TYPE = paramsJsonType + + export const parseRequestBodyToString = ({ requestType, params, apiRequestParamJsonType, raw }: unknown) => { + let result: string = '' + switch ((requestType || 'FORAMDATA').toString()) { + case 'RAW': { + //raw + // todo + result = raw + break + } + case 'JSON': { + //json + if (!params[0]?.hasOwnProperty('value')) keyRefs.value = 'name' + result = tranformJson(params, keyRefs, typeRefs) + if (apiRequestParamJsonType === 'ARRAY') { + //array + result = `[${result}]` + } + break + } + case 'XML': { + //xml + if (!params[0]?.hasOwnProperty('value')) keyRefs.value = 'name' + result = tranformXml(params, keyRefs, typeRefs) + break + } + } + return result + } \ No newline at end of file diff --git a/frontend/packages/common/src/components/apispace/code-snippet/util.ts b/frontend/packages/common/src/components/apispace/code-snippet/util.ts new file mode 100644 index 00000000..f9231d51 --- /dev/null +++ b/frontend/packages/common/src/components/apispace/code-snippet/util.ts @@ -0,0 +1,131 @@ +import * as Mock from 'mockjs' +import { PARAM_KEY_REF_TYPE, PARAM_LIST_TYPE, PARAM_LIS_ITEM_TYPE, PARAM_TYPE, PARAM_TYPE_REF_TYPE } from "./code-snippets.type" +const DEFAULT_PARAM_KEY_REF: PARAM_KEY_REF_TYPE = { + key: 'key', + type: 'type', + value: 'value' +} + + /** + * 将自定义列表转换为 xml + * @param list 列表 + * @param keyRefs 关键词映射 + * @param random 是否随机值 + * @returns xml 字符串 + */ + export function tranformXml( + list: PARAM_LIST_TYPE, + keyRefs: PARAM_KEY_REF_TYPE = DEFAULT_PARAM_KEY_REF, + typeRefs: PARAM_TYPE_REF_TYPE = {}, + random: boolean = false, + root: boolean = true, + parent?: boolean +) { + const { key, attribute, value, childKey, filter, type, arrayItemKey } = keyRefs + let result: string = root ? '' : '' + list.forEach((item: unknown) => { + if (filter && item[filter]) return + const tab: string = item[key] + if (!tab) return + const itemType: PARAM_TYPE = typeRefs[item[type]] + let text: string = '' + if (['array', 'object'].includes(itemType) && childKey && item[childKey]) { + //存在子层级 + text = tranformXml(item[childKey], keyRefs, typeRefs, random, false, itemType === 'array') + } else { + text = random === true ? getRandomDataByType(itemType) : item[value] + } + if (arrayItemKey && item[arrayItemKey]) { + result += `${text}` + } else { + result += `<${tab}${attribute && item[attribute] ? ` ${item[attribute]}` : ''}>${text}` + } + }) + return result +} +export function tranformJson( + list: PARAM_LIST_TYPE, + keyRefs: PARAM_KEY_REF_TYPE = DEFAULT_PARAM_KEY_REF, + typeRefs: PARAM_TYPE_REF_TYPE = {}, + random: boolean = false, + parent?: boolean +) { + const { key, value, childKey, type, filter, arrayItemKey } = keyRefs + const result: string[] = [] + list.forEach((item: PARAM_LIS_ITEM_TYPE) => { + if (filter && item[filter]) return + const tab: string = item[key] + if (!tab) return + const itemType: PARAM_TYPE = typeRefs[item[type]] + let text: string = '' + if (['array', 'object'].includes(itemType) && childKey && item[childKey]) { + //存在子层级 + text = tranformJson(item[childKey], keyRefs, typeRefs, random, itemType === 'array') + } else { + text = random === true ? getRandomDataByType(itemType) : item[value] + //将所有内容都转成字符串,用于后面注入 + if (typeof text === 'undefined') text = '' //用于兼容边界数据 + if (itemType === 'string') text = JSON.stringify(text) + } + if (arrayItemKey && item[arrayItemKey]) result.push(`${text}`) + else result.push(`"${tab}":${text}`) + }) + if (parent) return `[${result.join(',')}]` + return `{${result.join(',')}}` +} + + /** + * 将自定义列表转换为地址栏参数 + * @param list 列表 + * @param keyRefs 关键词映射 + * @param random 是否随机值 + * @returns key-value 结构字符串 + */ + export function tranformUrlParam( + list: PARAM_LIST_TYPE, + keyRefs: PARAM_KEY_REF_TYPE = DEFAULT_PARAM_KEY_REF, + typeRefs: PARAM_TYPE_REF_TYPE = {}, + random: boolean = false + ) { + const { key, value, filter, type } = keyRefs + const result: string[] = [] + list.forEach((item: unknown) => { + if (filter && item[filter]) return + const tab: string = item[key] + if (!tab) return + const itemType: PARAM_TYPE = typeRefs[item[type]] + + const text: string = random === true ? getRandomDataByType(itemType) : item[value] + result.push(`${tab}=${text}`) + }) + return result.join('&') //分隔符为& + } + /** + * 将自定义列表转换为 key-value 结构 + * @param list 列表 + * @param keyRefs 关键词映射 + * @param random 是否随机值 + * @returns + */ + export function tranformKeyValue( + list: PARAM_LIST_TYPE, + keyRefs: PARAM_KEY_REF_TYPE = DEFAULT_PARAM_KEY_REF, + typeRefs: PARAM_TYPE_REF_TYPE = {}, + random: boolean = false +) { + const { key, value, filter, type } = keyRefs + const result: string[] = [] + list.forEach((item: unknown) => { + if (filter && item[filter]) return + const tab: string = item[key] + if (!tab) return + const itemType: PARAM_TYPE = typeRefs[item[type]] + + const text: string = random === true ? getRandomDataByType(itemType) : item[value] + result.push(`${tab}:${text}`) + }) + return result.join('\n') //分隔符会换行 +} +export function getRandomDataByType(type: PARAM_TYPE) { + return Mock.Random[Object.keys(Mock.Random).includes(type) ? type : 'string'](0, 5) +} diff --git a/frontend/packages/common/src/components/apispace/response-example/index.module.css b/frontend/packages/common/src/components/apispace/response-example/index.module.css new file mode 100644 index 00000000..923058c8 --- /dev/null +++ b/frontend/packages/common/src/components/apispace/response-example/index.module.css @@ -0,0 +1,22 @@ +.tab-container{ + position: relative; + padding: 10px; + line-height: 20px; + font-size: 12px; + border: 1px solid var(--border-default-color); + border-radius: var(--DEFAULT_BORDER_RADIUS); +} +.code-span { + padding: 5px 10px; + color: #5f7d8b; + border-radius: var(--DEFAULT_BORDER_RADIUS); + border: 1px solid #EDEDED; + background-color: #f7f8fa; +} +.pre { + padding-top: 35px; + min-height: 130px; + max-height: 500px; + word-break: break-all; + overflow: auto; +} \ No newline at end of file diff --git a/frontend/packages/common/src/components/apispace/response-example/index.tsx b/frontend/packages/common/src/components/apispace/response-example/index.tsx new file mode 100644 index 00000000..c605f8b4 --- /dev/null +++ b/frontend/packages/common/src/components/apispace/response-example/index.tsx @@ -0,0 +1,111 @@ +import {useState, useEffect, useImperativeHandle} from 'react'; +import {AutoComplete, Empty, Tabs} from 'antd'; +import { ResultListType} from "@common/const/api-detail"; +import {Collapse} from "@common/components/postcat/api/Collapse"; +import {Box} from "@mui/material"; +import {Codebox} from "@common/components/postcat/api/Codebox"; +import { cloneDeep } from 'lodash-es'; + +export interface ResponseExampleCompoEditorApi { + getData: () => ResultListType[] | [] +} + +const DEFAULT_RESULT_LIST = [ + {id:'success',name:'成功示例',httpCode:'200',content:''}, + {id:'failed',name:'失败示例',httpCode:'200',content:''}, +] + +export const HTTP_STATUS_CODE = ['200', '403', '404', '410', '422', '500', '502', '503', '504'] + +export const CONTENT_TYPE_TYPE = [ + 'application/json', + 'application/x-www-form-urlencoded', + 'image/jpeg', + 'image/png', + 'multipart/form-data', + 'text/asp', + 'text/css', + 'text/html', + 'text/html; charset=UTF-8', + 'text/plain', + 'text/xml' +] + +export function ResponseExampleCompo ({ editorRef,title,detail,mode='view' }: {editorRef?: React.RefObject,title:string, detail:resultList[]}) { + const [resultDemos, setResultDemos] = useState([]); + + useImperativeHandle(editorRef, () => ({ + getData: () => { + return resultDemos||[] + } + })) + + useEffect(() => { + if(mode === 'view'){ + setResultDemos(detail); + }else{ + setResultDemos(detail?.length > 0 ? detail: cloneDeep(DEFAULT_RESULT_LIST)) + } + }, [detail]); + + const updateResultList = (id:string, type:'httpCode' | 'httpContentType'|'content',value:string) => { + setResultDemos((prevList)=>{ + for(let i = 0 ; i < prevList.length; i++){ + if(prevList[i].id === id){ + prevList[i][type] = value + return prevList + } + } + }) + } + + return ( + + + + {resultDemos && resultDemos?.map((item:ResultListType) => ( + +
+
+ {mode === 'view' ? + item.content ? HTTP Status Code: {item.httpCode}:'' + : ({label:code, value:code}))} + style={{ width: 200 }} + value={item.httpCode} + status={item.httpCode ? '' : 'error'} + onSelect={(value)=>updateResultList(item.id,'httpCode',value)} + placeholder="HTTP 状态码" + /> + } + {mode === 'view' ? + item.content ? Content-Type: {item.httpContentType || 'text/html;charset=UTF-8'}:'' + : ({label:type, value:type}))} + style={{ width: 200 }} + value={item.httpContentType || 'text/html;charset=UTF-8'} + onSelect={(value)=>updateResultList(item.id,'httpContentType',value)} + placeholder="默认 text/html;charset=UTF-8" + />} +
+ {mode === 'view' ? + <> + { item.content ? +
{item.content}
+ : + + } + : <> + updateResultList(item.id,'content',value)}/> + + } +
+
+ ))} +
+
+
+ ); +} + diff --git a/frontend/packages/common/src/components/postcat/ApiEdit.tsx b/frontend/packages/common/src/components/postcat/ApiEdit.tsx new file mode 100644 index 00000000..1bf6aa94 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/ApiEdit.tsx @@ -0,0 +1,257 @@ + +import {Collapse} from "./api/Collapse"; +import {forwardRef, useEffect, useImperativeHandle, useRef, useState} from "react"; +import {Select,Input,Space} from "antd"; +import { Box, Stack, ThemeProvider, createTheme } from "@mui/material" +import {ApiResponseEditor, ApiResponseEditorApi} from "./api/ApiManager/components/ApiResponseEditor"; +import {ApiRequestEditor, ApiRequestEditorApi} from "./api/ApiManager/components/ApiRequestEditor"; +import {ResponseExampleCompo, ResponseExampleCompoEditorApi} from "@common/components/apispace/response-example"; +import {ResultListType} from "@common/const/api-detail"; +import { SystemApiDetail, SystemInsideApiProxyHandle } from "@core/const/system/type"; +import SystemInsideApiProxy from "@core/pages/system/api/SystemInsideApiProxy"; +import ApiMatch from "./api/ApiPreview/components/ApiMatch"; +import {v4 as uuidv4} from 'uuid' + +const PROTOCOL_LIST = ['HTTP','HTTPS'] +const HTTP_METHOD_LIST = ['POST','GET','PUT', 'DELETE','HEAD','OPTIONS','PATCH'] +export interface ApiEditApi{ + getData:()=>(Promise<{apiInfo:Partial}|string|boolean> | undefined) +} + +interface DescriptionHandle{ + getData:()=>string +} +interface ApiNameProps{ + apiInfo:SystemApiDetail +} + +interface ApiNameHandle{ + getData:()=>string +} + +export const theme = createTheme({ + palette: { + primary: { + main: '#3D46F2', // 自定义主色调 + }, + text: { + primary: '#333', // 主要文字颜色 + secondary: '#333', // 次要文字颜色 + }, + // 添加其他颜色配置,如错误色、背景色等 + error: { + main: '#d32f2f', + }, + background: { + paper: '#fff', + default: '#f7f8fa', + }, + }, + transitions:{ + create:()=>'none' + }, + components:{ + MuiInput: { + styleOverrides: { + root: { + '&::placeholder': { + color: '#BBB', // 设置 placeholder 的颜色 + }, + '&:hover:not(.Mui-disabled):not(.Mui-focused):not(.Mui-error)': { + borderColor: '#3D46F2', // 设置 hover 时的边框颜色 + borderWidth: '1px', // 设置边框粗细 + }, + '&.Mui-focused': { + borderColor: '#3D46F2', // 设置选中时的边框颜色 + borderWidth: '1px', // 设置边框粗细 + }, + }, + }, + }, + MuiTextField: { + styleOverrides: { + root: { + '&::placeholder': { + color: '#BBB', // 设置 placeholder 的颜色 + }, + '&:hover .MuiOutlinedInput-notchedOutline':{ + borderColor: '#3D46F2', // 设置选中时的边框颜色 + borderWidth: '1px', // 设置边框粗细 + } + }, + }, + }, + MuiCheckbox: { + styleOverrides: { + root: { + '&:hover': { + backgroundColor: 'transparent', // 设置 hover 时的背景色为透明 + }, + '&:hover:before': { + backgroundColor: 'transparent', // 确保不透明度也为透明 + }, + transition: 'none', // 取消过渡效果 + }, + }, + }, + MuiButton:{ + styleOverrides: { + root: { + '&':{ + marginLeft:'0px', + padding:'3px 12px', + borderRadius:'4px' + } + } + } + }, + MuiSelect: { + styleOverrides: { + root: { + '&:hover:not(.Mui-disabled):not(.Mui-focused):not(.Mui-error)': { + borderColor: '#3D46F2', + borderWidth: '1px', + }, + '&:hover:not(.Mui-disabled):not(.Mui-focused):not(.Mui-error) .MuiOutlinedInput-notchedOutline': { + borderColor: '#3D46F2', + borderWidth: '1px', + }, + '&.Mui-focused': { + borderColor: '#3D46F2', + borderWidth: '1px', + }, + '&.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: '#3D46F2', + borderWidth: '1px', + }, + }, + }, + }, + MuiMenu:{ + styleOverrides: { + root:{ + '.MuiMenuItem-root:hover':{ + backgroundColor: '#EBEEF2', + }, + '.MuiMenuItem-root.Mui-selected':{ + backgroundColor: '#EBEEF2', + } + } + } + }, + MuiInputLabel: { + styleOverrides: { + root: { + color: '#BBB', // 设置 label 的颜色为灰色 + }, + }, + }, + } + }); + +export default function ApiEdit({apiInfo,editorRef,loaded,serviceId, teamId}:{apiInfo:SystemApiDetail,editorRef?:React.RefObject,loaded:boolean,serviceId:string, teamId:string}){ + const requestRef = useRef(null) + const responseRef = useRef(null) + const resultListRef = useRef(null) + const protocolOptionList = PROTOCOL_LIST.map((x)=>({label:x,value:x})) + const methodOptionList = HTTP_METHOD_LIST.map((x)=>({label:x,value:x})) + const [apiName,setApiName]=useState('') + const [resultList,setResultList] = useState([]) + const proxyRef = useRef(null) + const descriptionRef = useRef(null) + const apiNameRef = useRef(null) + + useImperativeHandle(editorRef, () => ({ + getData: () => { + return proxyRef.current?.validate().then((res)=>{ + const name = apiNameRef.current?.getData() + if(!name) return Promise.reject('请填写接口名称') + const newData :{apiInfo:Partial}= { + apiInfo:{ + info:{ + name, + description:descriptionRef.current?.getData(), + }, + proxy:res, + doc:{ + ...apiInfo?.doc, + requestParams:requestRef.current!.getData()!, + responseList:responseRef.current!.getData()!, + resultList:resultListRef.current!.getData()! + } + } + } + return Promise.resolve(newData) + }).catch((errInfo)=>Promise.reject(errInfo)) + } + })) + + useEffect(() => { + if(!apiInfo || Object.keys(apiInfo).length === 0) return + setApiName(apiInfo.name!) + setResultList(apiInfo?.doc?.resultList || []) + }, [apiInfo]); + + const Description = forwardRef((props,ref)=>{ + + const { initDescription } = props + const [description, setDescription] = useState(initDescription||'') + useImperativeHandle(ref, ()=>({ + getData:()=>description + })) + return ( + setDescription(e.target.value)} placeholder="请输入"/> + ) + }) + + const ApiName = forwardRef((props,ref)=>{ + const {apiInfo} = props + const [apiName, setApiName] = useState(apiInfo?.name || '') + useImperativeHandle(ref, ()=>({ + getData:()=>apiName + })) + return ( + <> + + + + + setApiName(e.target.value) } status={apiName ? '' : 'error'}/> + + ) + }) + + return( + <> + + + + + + + + + { + apiInfo?.match && apiInfo.match?.length > 0 && + {x.id = uuidv4();return x})} /> + } + + + + + + + + + + + + + + + + + + ) +} \ No newline at end of file diff --git a/frontend/packages/common/src/components/postcat/ApiPreview.module.css b/frontend/packages/common/src/components/postcat/ApiPreview.module.css new file mode 100644 index 00000000..b89c7694 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/ApiPreview.module.css @@ -0,0 +1,7 @@ +.api-url-disabled :global(.ant-input-search-button){ + cursor: not-allowed !important; + border-color: var(--border-color) !important; + color: rgba(0, 0, 0, 0.25) !important; + background-color: rgba(0, 0, 0, 0.04) !important; + box-shadow: none !important; +} \ No newline at end of file diff --git a/frontend/packages/common/src/components/postcat/ApiPreview.tsx b/frontend/packages/common/src/components/postcat/ApiPreview.tsx new file mode 100644 index 00000000..c17c596a --- /dev/null +++ b/frontend/packages/common/src/components/postcat/ApiPreview.tsx @@ -0,0 +1,158 @@ +import {useCallback, useEffect, useState} from "react"; +import Search from "antd/es/input/Search"; +import {Button, Space, Tooltip} from "antd"; +import CodeSnippetCompo from "@common/components/apispace/code-snippet"; +import {ApiDetail} from "@common/const/api-detail"; +import {flattenTree} from "@common/utils/postcat.tsx"; +import MessageBodyComponent, {RenderMessageBody} from "./api/ApiPreview/components/MessageBody"; +import HeaderFields from "./api/ApiPreview/components/HeaderFields"; +import {ResponseExampleCompo} from "@common/components/apispace/response-example"; +import {MoreSetting} from "./api/MoreSetting"; +import { + useMoreSettingHiddenConfig +} from "./api/ApiManager/components/MessageDataGrid/hooks/useMoreSettingHiddenConfig.ts"; +import {MessageType} from "./api/ApiManager/components/MessageDataGrid"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import { ThemeProvider } from "@mui/material"; +import { theme } from "./ApiEdit.tsx"; + +export const SearchBtn = ({entity}:{entity:unknown})=>{ + return ( + + 测试 API + + ) +} + +export default function ApiPreview(props:{testClick?:()=>void, entity:ApiDetail}){ + const {testClick,entity} = props + const {requestParams,responseList,resultList} = entity + const [requestBodyList, setRequestBodyList] = useState([]) + const [responseBodyList, setResponseBodyList] = useState([]) + const [currentMoreSettingParam, setCurrentMoreSettingParam] = useState(null) + + // const responseData = responseList?.[0] + // const responseParams = responseData?.responseParams?.headerParams + + useEffect(() => { + // setTimeout(()=>{ + // const element = document.querySelectorAll('.MuiDataGrid-main'); + // if(element?.length > 0){ + // for(const x of element){ + // x.childNodes[x.childNodes.length - 1 ].textContent === 'MUI X Missing license key' ? x.childNodes[x.childNodes.length - 1 ].textContent = '' :null + // } + // } + // },500) + + + setRequestBodyList( + flattenTree(requestParams?.bodyParams || [], 'childList', 'name') as unknown as RenderMessageBody[] + ) + setResponseBodyList( + flattenTree( + responseList?.[0]?.responseParams?.bodyParams || [], + 'childList', + 'name' + ) as unknown as RenderMessageBody[] + ) + }, [requestParams,responseList,resultList]); + + + const handleCloseMoreSetting = useCallback(() => { + setCurrentMoreSettingParam(null) + }, []) + + + const moreSettingHiddenConfig = useMoreSettingHiddenConfig({ + param: currentMoreSettingParam as unknown as RenderMessageBody, + // TODO: + messageType: 'Header' as MessageType, + readOnly: true + }) + + const handleTest = () => { + // testClick && testClick() + }; + + return (<> + + + {testClick && + + } + onSearch={handleTest} + /> + } + + { + requestParams?.headerParams?.length > 0 && + + } + + {requestBodyList?.length > 0 && + + } + + { + requestParams?.queryParams?.length > 0 && + + } + + { + requestParams?.restParams?.length > 0 && + + } + + {/*

请求示例代码

*/} + + + + + + + : undefined } + /> + + {resultList?.length > 0 && } + + { + responseList?.[0]?.responseParams?.headerParams?.length > 0 && + + } + + {responseBodyList?.length > 0 && + + } + + +
+ ) +} + diff --git a/frontend/packages/common/src/components/postcat/ApiTest.tsx b/frontend/packages/common/src/components/postcat/ApiTest.tsx new file mode 100644 index 00000000..8e2ec302 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/ApiTest.tsx @@ -0,0 +1,287 @@ +import { Box, Button, LinearProgress, Stack } from '@mui/material' +import { Allotment } from 'allotment' +import { memo, useCallback, useContext, useEffect, useImperativeHandle, useRef, useState } from 'react' +import { useAutoAnimate } from '@formkit/auto-animate/react' +import {HTTPMethod} from "./api/RequestMethod"; +import { + ApiBodyType, + ApiDetail, + BodyParamsType, + HeaderParamsType, + QueryParamsType, ResponseList, RestParamsType, + TestApiBodyType +} from "@common/const/api-detail"; +import {extractBraceContent, mapContentTypeToApiBodyType, syncUrlAndQuery} from "@common/utils/postcat.tsx"; +import {generateRow} from "./api/ApiManager/components/MessageDataGrid/constants.ts"; +import {RawParams} from "./api/ApiManager/components/ApiMessageBody"; +import {ParseCurlResult} from "@common/utils/curl.ts"; +import {ApiRequestTester, ApiRequestTesterApi} from "./api/ApiTest/components/ApiRequestTester"; +import {TestResponse} from "@common/hooks/useTest.ts"; +import {getDefaultApiInfo} from "./api/ApiManager/constants.ts"; +import {UriInput} from "./api/ApiManager/components/UriInput"; +import {TestControl} from "./api/ApiTest/components/TestControl"; +const Tester = memo(ApiRequestTester) +import 'allotment/dist/style.css' +import {ApiResponse} from "./api/ApiTest/components/ApiResponse"; + +type SafeAny = unknown +export interface ApiTestApiRef { + getTestMeta: () => SafeAny +} + +export default function ApiTest({ apiRef, apiInfo,loaded = true}: { apiRef?: React.RefObject ,apiInfo:ApiDetail,loaded?:boolean}) { + const [uri, setUri] = useState('') + const [httpMethod, setHttpMethod] = useState(HTTPMethod.POST) + const [testResponse, setTestResponse] = useState(null) + // const { apiInfo, loaded } = useContext>(ApiTabContext) + const testerApiRef = useRef(null) + const [parent] = useAutoAnimate() + // const testApiInfo:ApiDetail = apiInfo + const [isLoading, setIsLoading] = useState() + const [cancel,setCancel] = useState() + + + + // useEffect(() => { + // if (testApiInfo) { + // const data: SafeAny = testApiInfo + // const responseResult = { + // report: { + // general: { + // downloadRate: data.downloadRate, + // downloadSize: data.downloadSize, + // redirectTimes: data.redirectTimes, + // time: data.time, + // timingSummary: data.timingSummary + // }, + // request: { + // headers: testApiInfo.requestParams.headerParams?.map((item) => ({ + // value: item.paramAttr.example, + // key: item.name + // })), + // requestType: data.request.contentType, + // body: testApiInfo.requestParams.bodyParams, + // uri: testApiInfo.uri + // }, + // response: { + // headers: data.headers.map((item: SafeAny) => ({ key: item.name, value: item.value })), + // body: data.body, + // contentType: data.contentType, + // httpCode: data.statusCode, + // responseType: data.responseType + // } + // } + // } as unknown as TestResponse + // setTestResponse(responseResult) + // setHttpMethod(testApiInfo.method) + // testerApiRef.current?.updateHeaderDataGrid((testApiInfo.requestParams.headerParams as HeaderParamsType[]) || []) + // const apiBodyType: TestApiBodyType = testApiInfo.requestParams.bodyParams[0]?.contentType as TestApiBodyType + // const contentType = apiBodyType === ApiBodyType.Raw ? 'application/json' : 'application/x-www-form-urlencoded' + // testerApiRef.current?.updateRequestBody({ + // apiBodyType, + // contentType: contentType, + // data: + // apiBodyType === ApiBodyType.Raw + // ? testApiInfo.requestParams.bodyParams?.[0]?.binaryRawData + // : testApiInfo.requestParams.bodyParams + // }) + // setTimeout(() => { + // setUri(testApiInfo.uri) + // testerApiRef.current?.updateQueryDataGrid([]) + // testerApiRef.current?.updateRestDataGrid([]) + // }, 0) + // // updateTestApiInfo(null) + // } + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, [testApiInfo]) + + useEffect(() => { + if (apiInfo && loaded) { + setUri(apiInfo.uri) + setHttpMethod(apiInfo.method) + testerApiRef.current?.updateHeaderDataGrid((apiInfo.requestParams.headerParams as HeaderParamsType[]) || []) + const apiBodyType: TestApiBodyType = apiInfo.requestParams?.bodyParams[0]?.contentType as TestApiBodyType + const contentType = apiBodyType === ApiBodyType.Raw ? 'application/json' : 'application/x-www-form-urlencoded' + testerApiRef.current?.updateRequestBody({ + apiBodyType, + contentType: contentType, + data: + apiBodyType === ApiBodyType.Raw + ? apiInfo.requestParams?.bodyParams?.[0]?.binaryRawData + : apiInfo.requestParams?.bodyParams + }) + } + }, [apiInfo, loaded]) + + useImperativeHandle(apiRef, () => ({ + getTestMeta: () => { + const { + rest, + query, + headers, + body, + } = testerApiRef.current?.getEditMeta() || {} + return { + uri, + restParams: rest || [], + headersParams: (headers as HeaderParamsType[]) || [], + bodyParams: (body?.data as BodyParamsType[]) || [], + queryParams: query || [], + method: httpMethod, + requestType: body!.apiBodyType + } + } + })) + + const handleTest = async () => { + const { rest, headers, body} = testerApiRef.current?.getEditMeta() || {} + // const response = await test( + // { apiId, workspaceId, projectId }, + // { + // uri, + // restParams: rest || [], + // headersParams: (headers as HeaderParamsType[]) || [], + // bodyParams: (body?.data as BodyParamsType[]) || [], + // method: httpMethod, + // requestType: body!.apiBodyType + // } + // ) + // const response = {data:{}} + // if (response.data?.report?.request) { + // response.data.report.request.uri = uri + // } + // setTestResponse(response.data) + } + + const handleQueryChange = useCallback((queryList: QueryParamsType[]) => { + /** Can't use new URL due to potential non-standard URLs; reverting to pre-refactor code temporarily. */ + const result = syncUrlAndQuery(uri, queryList as SafeAny, { + nowOperate: 'query', + method: 'replace' + }) + if (result?.url) { + setUri(result.url) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const handleUriChange = (uri: string) => { + setUri(uri) + // if (activeTab?.path === quickTestRoute.path) { + // updateTab({ ...activeTab, name: uri || 'New Request', method: httpMethod } as TabRouteObject) + // } + if (uri) { + const restResult = extractBraceContent(uri) + const queryResult = syncUrlAndQuery(uri, []) + queryResult?.query?.length && testerApiRef.current?.updateQueryDataGrid(queryResult.query) + restResult?.length && + testerApiRef.current?.updateRestDataGrid(restResult.map((rest) => ({ name: rest })) as RestParamsType[]) + } + } + + const handleHttpMethodChange = (method: HTTPMethod) => { + setHttpMethod(method) + // if (activeTab?.path === quickTestRoute.path) { + // updateTab({ ...activeTab, name: uri || 'New Request', method } as TabRouteObject) + // } + } + + /** Execute this logic only during 'Quicktest' run. */ + const handleSaveApi = () => { + const { rest, query, headers, body, preScript = '', postScript = '' } = testerApiRef.current?.getEditMeta() || {} + const newApiInfo = getDefaultApiInfo() + const contentType = mapContentTypeToApiBodyType(body?.contentType ?? 'text/plain') + newApiInfo.uri = uri + newApiInfo.name = 'New Request' + newApiInfo.apiAttrInfo.requestMethod = httpMethod + newApiInfo.apiAttrInfo.contentType = contentType + newApiInfo.requestParams = { + bodyParams: body?.data, + headerParams: headers, + queryParams: query, + restParams: rest + } as SafeAny + if (testResponse) { + const response = testResponse.report.response + const responseHeader = response?.headers.map((header) => { + return generateRow({ + name: header.key, + paramAttr: { + example: header.value + } + }) + }) + const responseList = [ + { + // TODO: response JSON? + contentType: ApiBodyType.Raw, + responseParams: { + bodyParams: [RawParams(response?.body || '')], + headerParams: responseHeader || [], + queryParams: [], + restParams: [] + } + } + ] + newApiInfo.responseList = responseList as unknown as ResponseList[] + } + } + + const handleCURLParse = (cURLResult: ParseCurlResult) => { + setHttpMethod(HTTPMethod[cURLResult.method as keyof typeof HTTPMethod]) + /** cURLResult.body */ + const headers = Object.keys(cURLResult.headers).map((key) => ({ + name: key, + paramAttr: { + example: cURLResult.headers[key] + } + })) + testerApiRef.current?.updateHeaderDataGrid((headers as HeaderParamsType[]) || []) + testerApiRef.current?.updateRequestBodyWithCurlInfo(cURLResult) + setTimeout(() => { + setUri(cURLResult.url) + testerApiRef.current?.updateQueryDataGrid([]) + testerApiRef.current?.updateRestDataGrid([]) + }, 0) + } + + + return ( + + + + + + + + + + + + + + + + + + + + + + {isLoading ? : null} + + + + + + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/Indicator/index.tsx b/frontend/packages/common/src/components/postcat/Indicator/index.tsx new file mode 100644 index 00000000..ff611454 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/Indicator/index.tsx @@ -0,0 +1,12 @@ +import type { SxProps, Theme } from '@mui/material' +import { Box } from '@mui/material' + +export interface IndicatorProps { + color?: string + sx?: SxProps +} + +export function Indicator({ color = 'pink', sx }: IndicatorProps): JSX.Element { + const size = '6px' + return +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiMessageBody/components/Binary.tsx b/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiMessageBody/components/Binary.tsx new file mode 100644 index 00000000..30607e7a --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiMessageBody/components/Binary.tsx @@ -0,0 +1,17 @@ +import { TextField } from '@mui/material' +import { SyntheticEvent } from 'react' + +export function RequestBodyBinary({ value, onChange }: { value: string; onChange: (value: string) => void }) { + return ( + { + onChange((evt.target as HTMLInputElement).value) + }} + fullWidth + /> + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiMessageBody/components/Raw.tsx b/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiMessageBody/components/Raw.tsx new file mode 100644 index 00000000..5af5461e --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiMessageBody/components/Raw.tsx @@ -0,0 +1,10 @@ +import {Codebox} from "../../../../Codebox"; + +interface RequestBodyRawProps { + value: string + onChange: (value: string) => void +} +export function RequestBodyRaw({ value, onChange }: RequestBodyRawProps) { + // @ts-ignore + return +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiMessageBody/constants.ts b/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiMessageBody/constants.ts new file mode 100644 index 00000000..49750123 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiMessageBody/constants.ts @@ -0,0 +1,82 @@ + +import { ApiBodyType, ApiParamsType } from "@common/const/api-detail" + +export type ApiBodyTypeLabel = 'Form-Data' | 'JSON' | 'XML' | 'Raw' | 'Binary' + +export type ApiBodyTypeOption = { + key: ApiBodyTypeLabel + value: ApiBodyType + element: React.ReactNode +} + +export type ApiParamsTypeOption = { + key: keyof typeof ApiParamsType + value: ApiParamsType +} + +export const ApiParamsTypeOptions: ApiParamsTypeOption[] = [ + { + key: 'string', + value: ApiParamsType.string + }, + { + key: 'file', + value: ApiParamsType.file + }, + { + key: 'json', + value: ApiParamsType.json + }, + { + key: 'int', + value: ApiParamsType.int + }, + { + key: 'float', + value: ApiParamsType.float + }, + { + key: 'double', + value: ApiParamsType.double + }, + { + key: 'date', + value: ApiParamsType.date + }, + { + key: 'datetime', + value: ApiParamsType.datetime + }, + { + key: 'boolean', + value: ApiParamsType.boolean + }, + { + key: 'byte', + value: ApiParamsType.byte + }, + { + key: 'short', + value: ApiParamsType.short + }, + { + key: 'long', + value: ApiParamsType.long + }, + { + key: 'array', + value: ApiParamsType.array + }, + { + key: 'object', + value: ApiParamsType.object + }, + { + key: 'number', + value: ApiParamsType.number + }, + { + key: 'null', + value: ApiParamsType.null + } +] diff --git a/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiMessageBody/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiMessageBody/index.tsx new file mode 100644 index 00000000..f05b5532 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiMessageBody/index.tsx @@ -0,0 +1,263 @@ +import {Box, FormControl, FormControlLabel, MenuItem, Radio, RadioGroup, Select, SelectChangeEvent} from '@mui/material' +import {ApiBodyTypeOption} from './constants' +import {ChangeEvent, SetStateAction, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react' +import {RequestBodyRaw} from './components/Raw' +import {RequestBodyBinary} from './components/Binary' +import {ApiBodyType, BodyParamsType} from "@common/const/api-detail"; +import {generateId} from "@common/utils/postcat.tsx"; +import {MessageDataGrid, MessageDataGridApi} from "../MessageDataGrid"; + +export interface ApiMessageBodyApi { + getBodyMeta: () => { + contentType: ApiBodyType + bodyParams: Partial[] + } +} + +interface ApiBodyParamsTypeProps { + mode: 'request' | 'response' + bodyApiRef?: React.RefObject +} + +export function RawParams(value: string) { + return { + apiUuid: generateId(), + binaryRawData: value, + childList: [], + contentType: null, + dataType: 0, + dataTypeValue: '', + description: '', + id: generateId(), + isDefault: null, + isRequired: 0, + name: '', + orderNo: 0, + paramAttr: null, + paramType: 0, + parentId: 0, + partType: 1, + responseUuid: '', + structureId: 0, + structureParamId: '' + } +} + +export function ApiMessageBody({ apiInfo=null, loaded, mode, bodyApiRef }: ApiBodyParamsTypeProps) { + const [apiBodyTypeValue, setApiBodyTypeValue] = useState(ApiBodyType.JSON) + + const [apiFormData, setApiFormData] = useState([]) + const [apiJson, setApiJson] = useState([]) + const [jsonType, setJsonType] = useState(ApiBodyType.JSON) + const [apiXml, setApiXml] = useState([]) + const [apiRaw, setApiRaw] = useState('') + const [apiBinary, setApiBinary] = useState('') + + const jsonTableApiRef = useRef(null) + const formDataRef = useRef(null) + const xmlRef = useRef(null) + useImperativeHandle(bodyApiRef, () => ({ + getBodyMeta: () => { + const bodyParams: BodyParamsType[] = [] + if ([ApiBodyType.JSON, ApiBodyType.FormData, ApiBodyType.XML].includes(apiBodyTypeValue)) { + const targetRef = { + [ApiBodyType.JSON]: jsonTableApiRef, + [ApiBodyType.FormData]: formDataRef, + [ApiBodyType.XML]: xmlRef + }[apiBodyTypeValue as ApiBodyType.JSON | ApiBodyType.FormData | ApiBodyType.XML] + bodyParams.push(...(targetRef.current?.getEditMeta() as BodyParamsType[])) + } else if ([ApiBodyType.Raw, ApiBodyType.Binary].includes(apiBodyTypeValue)) { + bodyParams.push(RawParams(apiBodyTypeValue === ApiBodyType.Raw ? apiRaw : apiBinary) as unknown as BodyParamsType) + } + + return { + contentType: jsonType === ApiBodyType.JSONArray ? ApiBodyType.JSONArray : apiBodyTypeValue, + bodyParams + } + } + })) + + useEffect(() => { + // setTimeout(()=>{ + // const element = document.querySelectorAll('.MuiDataGrid-main'); + // if(element?.length > 0){ + // for(const x of element){ + // x.childNodes[x.childNodes.length - 1 ].textContent === 'MUI X Missing license key' ? x.childNodes[x.childNodes.length - 1 ].textContent = '' :null + // } + // } + // },500) + }, []); + + useEffect(() => { + if (loaded && (apiInfo || apiInfo === null)) { + let data: BodyParamsType[] | null = null + let contentType = ApiBodyType.JSON + if (mode === 'request') { + data = apiInfo?.requestParams?.bodyParams || [] + contentType = apiInfo?.apiAttrInfo?.contentType || ApiBodyType.JSON + } else if (mode === 'response') { + data = (apiInfo?.responseList?.[0]?.responseParams?.bodyParams as unknown as BodyParamsType[]) || [] + contentType = apiInfo?.responseList?.[0]?.contentType || ApiBodyType.JSON + } + if (contentType === ApiBodyType.JSONArray) { + contentType = ApiBodyType.JSON + setJsonType(ApiBodyType.JSONArray) + } + setApiBodyTypeValue(contentType) + const updaterMap = { + [ApiBodyType.FormData]: setApiFormData, + [ApiBodyType.JSON]: setApiJson, + [ApiBodyType.JSONArray]: setApiJson, + [ApiBodyType.XML]: setApiXml, + [ApiBodyType.Raw]: setApiRaw, + [ApiBodyType.Binary]: setApiBinary + } + const DataGridType = [ApiBodyType.FormData, ApiBodyType.JSON, ApiBodyType.JSONArray, ApiBodyType.XML] + if (DataGridType.includes(contentType)) { + DataGridType.forEach((type) => { + const updater: (value: SetStateAction) => void = updaterMap[type] as ( + value: SetStateAction + ) => void + if (+type === +contentType) { + updater([...((data || []) as BodyParamsType[])]) + } else { + const JsonType = [ApiBodyType.JSON, ApiBodyType.JSONArray] + if (!(JsonType.includes(contentType) && JsonType.includes(type))) { + updater([]) + } + } + }) + } + const StringType = [ApiBodyType.Raw, ApiBodyType.Binary] + if (StringType.includes(contentType)) { + StringType.forEach((type) => { + const stringUpdater: (value: SetStateAction) => void = updaterMap[type] as ( + value: SetStateAction + ) => void + stringUpdater(data?.[0]?.binaryRawData || '') + }) + DataGridType.forEach((type) => { + const updater: (value: SetStateAction) => void = updaterMap[type] as ( + value: SetStateAction + ) => void + updater([]) + }) + } + } + }, [apiInfo, loaded, mode]) + + const apiBodyTypeList: ApiBodyTypeOption[] = useMemo(() => { + return [ + mode === 'request' && { + key: 'Form-Data', + value: ApiBodyType.FormData, + element: ( + + ) + }, + { + key: 'JSON', + value: ApiBodyType.JSON, + element: ( + + ) + }, + { + key: 'XML', + value: ApiBodyType.XML, + element: ( + + ) + }, + { + key: 'Raw', + value: ApiBodyType.Raw, + element: + }, + { + key: 'Binary', + value: ApiBodyType.Binary, + element: + } + ].filter((type) => type) as ApiBodyTypeOption[] + }, [apiBinary, apiFormData, apiJson, apiRaw, apiXml, mode]) + + const handleApiBodyTypeValueChange = (event: ChangeEvent) => { + setApiBodyTypeValue(+(event.target as HTMLInputElement).value) + } + + const handleJsonTypeChange = (event: SelectChangeEvent) => { + setJsonType(event.target.value as unknown as ApiBodyType.JSON | ApiBodyType.JSONArray) + } + + return ( + + + + + {apiBodyTypeList.map((apiBodyType) => ( + } + label={apiBodyType.key} + /> + ))} + + + {[ApiBodyType.JSON].includes(apiBodyTypeValue) ? ( + + + + ) : null} + + {apiBodyTypeList.map((apiBodyType) => ( + + ))} + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiProxyEditor/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiProxyEditor/index.tsx new file mode 100644 index 00000000..4f95d2d8 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiProxyEditor/index.tsx @@ -0,0 +1,48 @@ +import { Box, useTheme } from '@mui/material' +import {ApiBodyType, BodyParamsType} from "@common/const/api-detail"; +import {RenderMessageBody} from "@common/components/postcat/api/apiManager/components/MessageDataGrid"; +export interface ApiProxyEditorApi { + getEditMeta: () => Partial[] +} + + +interface ApiProxyEditorProps { +// onChange?: (rows: T[]) => void +// initialRows?: T[] | null +// onDirty?: () => void + loading?: boolean +// messageType?: MessageType +// contentType: ContentType +// isMoreSettingReadOnly?: boolean +// apiRef?: RefObject +} + +export function ApiProxyEditor(props: ApiProxyEditorProps) { + const { + // onChange, + // initialRows, + // onDirty, + loading = false, + // contentType, + // messageType, + // isMoreSettingReadOnly, + // apiRef, + // loaded + } = props + const theme = useTheme() + + + return ( + + + + + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiRequestEditor/components/constants.ts b/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiRequestEditor/components/constants.ts new file mode 100644 index 00000000..8a1be505 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiRequestEditor/components/constants.ts @@ -0,0 +1,65 @@ +interface RequestHeader { + key: string + forbidden: boolean +} + +export const RequestHeaders: RequestHeader[] = [ + { key: 'Authorization', forbidden: false }, + { key: 'Accept', forbidden: false }, + { key: 'Accept-Language', forbidden: false }, + { key: 'Access-Control-Request-Headers', forbidden: true }, + { key: 'Access-Control-Request-Method', forbidden: true }, + { key: 'Accept-Charset', forbidden: true }, + { key: 'Accept-Encoding', forbidden: true }, + { key: 'Cache-Control', forbidden: false }, + { key: 'Content-MD5', forbidden: false }, + { key: 'Content-Type', forbidden: false }, + { key: 'Cookie', forbidden: false }, + { key: 'Content-Length', forbidden: true }, + { key: 'Content-Transfer-Encoding', forbidden: true }, + { key: 'Date', forbidden: true }, + { key: 'Expect', forbidden: true }, + { key: 'From', forbidden: false }, + { key: 'Host', forbidden: true }, + { key: 'If-Match', forbidden: false }, + { key: 'If-Modified-Since', forbidden: false }, + { key: 'If-None-Match', forbidden: false }, + { key: 'If-Range', forbidden: false }, + { key: 'If-Unmodified-Since', forbidden: false }, + { key: 'Keep-Alive', forbidden: true }, + { key: 'Max-Forwards', forbidden: false }, + { key: 'Origin', forbidden: true }, + { key: 'Pragma', forbidden: false }, + { key: 'Proxy-Authorization', forbidden: false }, + { key: 'Range', forbidden: false }, + { key: 'Referer', forbidden: true }, + { key: 'TE', forbidden: true }, + { key: 'Trailer', forbidden: true }, + { key: 'Transfer-Encoding', forbidden: true }, + { key: 'Upgrade', forbidden: true }, + { key: 'User-Agent', forbidden: true }, + { key: 'Via', forbidden: true }, + { key: 'Warning', forbidden: false }, + { key: 'X-Requested-With', forbidden: false }, + { key: 'X-Do-Not-Track', forbidden: false }, + { key: 'DNT', forbidden: false }, + { key: 'x-api-key', forbidden: false }, + { key: 'Connection', forbidden: true } +] + +/** + * https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name + */ +export const ForbiddenHeaderName = [ + 'age', + 'via', + 'accept-ranges', + 'nginx-hit', + 'referrer-policy', + 'location', + 'content-security-policy', + 'strict-transport-security', + 'server', + 'vary', + ...RequestHeaders.filter((header) => header.forbidden).map((val) => val.key.toLowerCase()) +] diff --git a/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiRequestEditor/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiRequestEditor/index.tsx new file mode 100644 index 00000000..dd28b41b --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiRequestEditor/index.tsx @@ -0,0 +1,178 @@ +import { Box, Grow, Tab, Tabs, Typography, useTheme } from '@mui/material' +import {ReactNode, SyntheticEvent, useEffect, useImperativeHandle, useRef, useState} from 'react' +import { + ApiBodyType, ApiDetail, + BodyParamsType, + HeaderParamsType, + QueryParamsType, + RestParamsType +} from "@common/const/api-detail"; +import {MessageDataGrid, MessageDataGridApi} from "../MessageDataGrid"; +import {Indicator} from "../../../../Indicator"; +import { ApiMessageBody, ApiMessageBodyApi } from '../ApiMessageBody'; + +export interface ApiRequestEditorApi { + getData: () => { + bodyParams?: { + contentType: ApiBodyType + bodyParams: Partial[] + } + headerParams: HeaderParamsType[] + queryParams: QueryParamsType[] + restParams: RestParamsType[] + } +} + +interface ApiRequestEditorTab { + label: string + element: ReactNode + dirty: boolean +} + +export function ApiRequestEditor({ editorRef ,apiInfo=null,loaded}: { editorRef?: React.RefObject ,apiInfo:ApiDetail,loaded:boolean}) { + const [apiHeaders, setApiHeaders] = useState([]) + const [apiQuery, setApiQuery] = useState([]) + const [apiRest, setApiRest] = useState([]) + + const headersRef = useRef(null) + const bodyRef = useRef(null) + const queryRef = useRef(null) + const restRef = useRef(null) + + const [innerLoaded,setInnerLoaded] = useState(false) + useImperativeHandle(editorRef, () => ({ + getData: () => { + return { + bodyParams: bodyRef.current?.getBodyMeta()?.bodyParams.map((x)=>({...x,contentType:bodyRef.current?.getBodyMeta().contentType})), + headerParams: (headersRef.current?.getEditMeta() as HeaderParamsType[]) || [], + queryParams: (queryRef.current?.getEditMeta() as QueryParamsType[]) || [], + restParams: (restRef.current?.getEditMeta() as RestParamsType[]) || [] + } + } + })) + + useEffect(() => { + if (loaded && (apiInfo || apiInfo === null)) { + setApiQuery(apiInfo?.requestParams?.queryParams || []) + setApiRest(apiInfo?.requestParams?.restParams || []) + setApiHeaders((apiInfo?.requestParams?.headerParams as unknown as HeaderParamsType[]) || []) + setInnerLoaded(true) + } + }, [apiInfo, loaded]) + + const tabs: ApiRequestEditorTab[] = [ + { + label: '请求头部', + element: ( + + ), + dirty: false + }, + { + label:'请求体', + element: , + dirty: false + }, + { + label: 'Query 参数', + element: ( + + ), + dirty: false + }, + { + label: 'REST 参数', + element: ( + + ), + dirty: false + } + ] + + // FIXME: + const [tabValue, setTabValue] = useState(tabs[0].label) + + const theme = useTheme() + + const handleChange = (_event: SyntheticEvent, newValue: string) => { + setTabValue(newValue) + } + + const tabHeight = '30px' + + return ( + + + {tabs.map((tab) => ( + + {tab.label} + + + + + + + } + sx={{ + textAlign: 'left', + padding: theme.spacing(1), + minWidth: 'auto', + minHeight: tabHeight + }} + /> + ))} + + + {tabs.map((tab) => ( + + ))} + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiResponseEditor/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiResponseEditor/index.tsx new file mode 100644 index 00000000..a787fca4 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiManager/components/ApiResponseEditor/index.tsx @@ -0,0 +1,144 @@ +import { Box, Grow, Tab, Tabs, Typography, useTheme } from '@mui/material' +import { ReactNode, SyntheticEvent, useContext, useEffect, useImperativeHandle, useRef, useState } from 'react' +import { MessageDataGrid, MessageDataGridApi } from '../MessageDataGrid' +import {ApiBodyType, BodyParamsType, HeaderParamsType} from "@common/const/api-detail"; +import {Indicator} from "../../../../Indicator"; +import { v4 as uuidv4} from 'uuid' +import { ApiMessageBody, ApiMessageBodyApi } from '../ApiMessageBody'; + +interface ApiRequestEditorTab { + label: string + element: ReactNode + dirty: boolean +} + +export interface ApiResponseEditorApi { + getData: () => { + bodyParams?: { + contentType: ApiBodyType + bodyParams: Partial[] + } + headerParams: HeaderParamsType[] + } +} + +export function ApiResponseEditor({ editorRef ,apiInfo=null, loaded}: { editorRef?: React.RefObject }) { + const [apiHeaders, setApiHeaders] = useState([]) + const [innerLoaded, setInnerLoaded] = useState(false) + const headersRef = useRef(null) + const bodyRef = useRef(null) + + useEffect(() => { + if (loaded && (apiInfo || apiInfo === null)) { + setApiHeaders((apiInfo?.responseList?.[0]?.responseParams?.headerParams as unknown as MessageBody[]) || []) + setInnerLoaded(true) + } + }, [apiInfo, loaded]) + + useImperativeHandle(editorRef, () => ({ + getData: () => { + const bodyData = bodyRef.current?.getBodyMeta() + const uuid = uuidv4() + return ([{ + id:uuid, + responseUuid:uuid, + httpCode:bodyData?.contentType, + responseParams:{ + bodyParams: bodyData?.bodyParams, + headerParams: (headersRef.current?.getEditMeta() as HeaderParamsType[]) || [] + } + } + ]) + } + })) + + const tabs: ApiRequestEditorTab[] = [ + { + label: '返回头部', + element: ( + + ), + dirty: false + }, + { + label: '返回值', + element: , + dirty: false + } + ] + + // FIXME: devlop value + const [tabValue, setTabValue] = useState(tabs[1].label) + + const theme = useTheme() + + const handleChange = (_event: SyntheticEvent, newValue: string) => { + setTabValue(newValue) + } + + const tabHeight = '30px' + + return ( + + + {tabs.map((tab) => ( + + {tab.label} + + + + + + + } + sx={{ + textAlign: 'left', + padding: theme.spacing(1), + minWidth: 'auto', + minHeight: tabHeight + }} + /> + ))} + + + {tabs.map((tab) => ( + + ))} + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiManager/components/EditableDataGrid/components/controls/DataGridTextField/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiManager/components/EditableDataGrid/components/controls/DataGridTextField/index.tsx new file mode 100644 index 00000000..61fdb175 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiManager/components/EditableDataGrid/components/controls/DataGridTextField/index.tsx @@ -0,0 +1,24 @@ +import { DataGridTextFieldProps } from '@common/components/postcat/api/ApiManager/components/EditableDataGrid' +import { memo, ChangeEvent } from 'react' +import { TextField as MuiTextField } from '@mui/material' + +interface TextFieldProps { + rowId: string + defaultValue: string + onChange: (event: ChangeEvent) => void + placeholder?: string +} + +export const TextField = ({ rowId, defaultValue, onChange, placeholder }: TextFieldProps) => { + return ( + + ) +} + +export const DataGridTextField = memo(TextField) diff --git a/frontend/packages/common/src/components/postcat/api/ApiManager/components/EditableDataGrid/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiManager/components/EditableDataGrid/index.tsx new file mode 100644 index 00000000..b0e8c796 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiManager/components/EditableDataGrid/index.tsx @@ -0,0 +1,86 @@ + +import { Box, SxProps, TextFieldProps, Theme } from '@mui/material' +import { HTMLAttributes, KeyboardEvent } from 'react' + +export const EditableDataGridSx = { + '& .MuiTextField-root': { + input: { + border: 'none', + width: '100%', + paddingLeft: 0, + paddingRight: 0 + } + }, + '& .MuiAutocomplete-root': { + '& .MuiInputBase-root.MuiInputBase-sizeSmall': { + input: { + padding: 0 + }, + paddingLeft: 0, + paddingRight: 0 + }, + '& .MuiAutocomplete-input': { + padding: 0 + }, + input: { + paddingLeft: 0, + paddingRight: 0 + } + }, + '& .MuiInput-underline:after': { + borderBottom: 'none' + }, + '& .MuiInput-underline:before': { + borderBottom: 'none' + }, + '&:hover .MuiInput-underline:before': { + borderBottom: 'none' + }, + '& .MuiInputBase-root': { + width: '100%', + border: 'none', + paddingLeft: 0, + paddingRight: 0 + }, + '& .MuiOutlinedInput-root': { + '& fieldset': { + border: 'none' + } + } +} + +/** Prevents 'Select All' in DataGridPro when selecting text in TextField. */ +const handleDataGridTextFieldKeydown = (evt: KeyboardEvent) => { + if ((evt.metaKey || evt.ctrlKey) && evt.key === 'a') { + evt.stopPropagation() + } +} + +export const DataGridTextFieldProps: TextFieldProps = { + fullWidth: true, + variant: 'outlined', + onKeyDown: handleDataGridTextFieldKeydown, + sx: { paddingLeft: 1 } +} + +export const DataGridAutoCompleteProps = { + disableClearable: true, + sx: { + width: 300, + padding: 0, + '& .MuiAutocomplete-endAdornment': { + display: 'none' + } + } as SxProps +} + +export function AutoCompleteOption(props: HTMLAttributes, option: { label: string } | string) { + const title = typeof option === 'string' ? option : option.label + return ( + + + {title} + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiManager/components/MessageDataGrid/constants.ts b/frontend/packages/common/src/components/postcat/api/ApiManager/components/MessageDataGrid/constants.ts new file mode 100644 index 00000000..ef2e6993 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiManager/components/MessageDataGrid/constants.ts @@ -0,0 +1,17 @@ + +import {generateId} from "@common/utils/postcat.tsx"; + +type SafeAny = unknown +export function generateRow(data: SafeAny = {}) { + return Object.assign({ + id: generateId(), + name: '', + dataType: null, + isRequired: 1, + description: '', + paramAttr: { + example: '' + }, + childList: [] + }, data) +} \ No newline at end of file diff --git a/frontend/packages/common/src/components/postcat/api/ApiManager/components/MessageDataGrid/hooks/useMoreSettingHiddenConfig.ts b/frontend/packages/common/src/components/postcat/api/ApiManager/components/MessageDataGrid/hooks/useMoreSettingHiddenConfig.ts new file mode 100644 index 00000000..6bf9e5e2 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiManager/components/MessageDataGrid/hooks/useMoreSettingHiddenConfig.ts @@ -0,0 +1,41 @@ +import {MessageType, RenderMessageBody} from "../index.tsx"; +import {isNil} from "@common/utils/postcat.tsx"; +import {ApiParamsType} from "@common/const/api-detail"; + +interface UseMoreSettingHiddenConfigProps { + param: RenderMessageBody + messageType: MessageType + readOnly: boolean +} + +export const useMoreSettingHiddenConfig = ({ param, messageType, readOnly }: UseMoreSettingHiddenConfigProps) => { + let paramLength = false + let valueEnum = false + let value = false + if (messageType !== 'Body') { + paramLength = true + value = true + } else { + if (readOnly) { + paramLength = isNil(param.paramAttr.minLength || param.paramAttr.maxLength) + valueEnum = !param?.paramAttr?.paramValueList?.length + } else { + paramLength = param?.dataType !== ApiParamsType.string + valueEnum = [ApiParamsType.null, ApiParamsType.boolean].includes(param?.dataType as ApiParamsType) + value = ![ + ApiParamsType.int, + ApiParamsType.float, + ApiParamsType.double, + ApiParamsType.short, + ApiParamsType.long, + ApiParamsType.number + ].includes(param?.dataType as ApiParamsType) + } + } + return { + paramLength, + valueEnum, + value, + example: readOnly && !param?.paramAttr?.example + } +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiManager/components/MessageDataGrid/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiManager/components/MessageDataGrid/index.tsx new file mode 100644 index 00000000..0ffc26d3 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiManager/components/MessageDataGrid/index.tsx @@ -0,0 +1,633 @@ +import { Autocomplete, Box, Checkbox, LinearProgress, TextField, Typography, useTheme } from '@mui/material' +import { + DataGridPro, + DataGridProProps, + GridColDef, + GridDataGroupNode, + GridRenderEditCellParams, + GridRowModes, + GridRowModesModel, + GridRowParams, + useGridApiRef +} from '@mui/x-data-grid-pro' +import { + ChangeEvent, + RefObject, + SyntheticEvent, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useState +} from 'react' +import { ApiParamsTypeOptions } from '../ApiMessageBody/constants' +import { RequestHeaders } from '../ApiRequestEditor/components/constants' +import {ApiParamsType, BodyParamsType, ParamAttrType, commonTableSx} from "@common/const/api-detail"; +import { + determineCheckState, + flattenTree, + generateId, + getActionColWidth, + isNil, + traverse +} from "@common/utils/postcat.tsx"; +import {MoreSetting} from "../../../MoreSetting"; +import { + AutoCompleteOption, + DataGridAutoCompleteProps, + DataGridTextFieldProps, + EditableDataGridSx +} from "../EditableDataGrid"; +import {collapseTableSx} from "../../../PreviewTable"; +import {IconButton} from "../../../IconButton"; +import {Icon} from "../../../Icon"; +import {useMoreSettingHiddenConfig} from "./hooks/useMoreSettingHiddenConfig.ts"; + +export interface RenderMessageBody extends BodyParamsType { + path?: string[] + childList?: RenderMessageBody[] + parent?: RenderMessageBody | null + __globalIndex__?: number + __levelIndex__?: number + __raw__?: BodyParamsType + __hasSiblingLeaf__?: boolean +} + +export type MessageType = 'Body' | 'Header' | 'Query' | 'REST' +export type ContentType = 'FormData' | 'JSON' | 'XML' | 'Headers' + +export interface MessageDataGridApi { + getEditMeta: () => Partial[] +} + +interface MessageDataGridProps { + onChange?: (rows: T[]) => void + initialRows?: T[] | null + onDirty?: () => void + loading?: boolean + messageType?: MessageType + contentType: ContentType + isMoreSettingReadOnly?: boolean + apiRef?: RefObject +} + +const groupingColDef: DataGridProProps['groupingColDef'] = { + headerName: '', + width: 40, + resizable: false, + renderCell: () => <> +} + +declare type CheckedStatus = 'checked' | 'unchecked' | 'indeterminate' + +export function MessageDataGrid(props: MessageDataGridProps) { + const { + onChange, + initialRows, + onDirty, + loading = false, + contentType, + messageType, + isMoreSettingReadOnly, + apiRef, + loaded + } = props + + const [rows, setRows] = useState([]) + + const [renderRows, setRenderRows] = useState([]) + + const [currentMoreSettingParam, setCurrentMoreSettingParam] = useState(null) + + const [rowModesModel, setRowModesModel] = useState({}) + + const [dirty, setDirty] = useState(false) + + const [openMoreSettingDialog, setOpenMoreSettingDialog] = useState(false) + + const [selectAll, setSelectAll] = useState('unchecked') + + const [innerLoaded, setInnerLoaded] = useState(false) + + const EmptyRow = useCallback( + (val: string = ''): BodyParamsType => { + const id = generateId() + const name = val + let dataType = null + if (messageType === 'Body') { + dataType = ApiParamsType.string + } + + return { + id, + name, + dataType, + isRequired: 1, + description: '', + paramAttr: { + example: '' + }, + childList: [] + } as unknown as BodyParamsType + }, + [messageType] + ) + + useEffect(() => { + dirty && onDirty?.() + }, [dirty, onDirty]) + + const tableApiRef = useGridApiRef() + + const [showRootIndent, setShowRootIndent] = useState(false) + + const updateSelectAll = useCallback(() => { + const isRequiredList: { + isRequired: boolean | 0 | 1 + }[] = [] + Object.values(tableApiRef.current.state.editRows).forEach((row) => { + isRequiredList.push({ + isRequired: !!((row as unknown)?.isRequired?.value as boolean) + }) + }) + setSelectAll(determineCheckState(isRequiredList)) + }, [tableApiRef]) + + useEffect(() => { + if (initialRows && loaded && !innerLoaded) { + let updateRows = [...initialRows,EmptyRow()] + if (!updateRows?.length && contentType !== 'XML') { + updateRows = [EmptyRow()] + } + if (!updateRows?.length && contentType == 'XML') { + const root = EmptyRow('root') + root.childList = [EmptyRow()] + updateRows = [root] + } + setRows(updateRows) + updateSelectAll() + setDirty(false) + onChange?.(updateRows) + setInnerLoaded(true) + } + }, [EmptyRow, contentType, initialRows, loaded,innerLoaded, onChange, updateSelectAll]) + + useEffect(() => { + const neoRenderRows = flattenTree( + rows.map((i) => ({ ...i, __reorder__: i.name })), + 'childList', + 'id' + ) + for (const row of rows) { + if (row.childList?.length) { + setShowRootIndent(true) + break + } + } + setRenderRows(neoRenderRows) + setRowModesModel(neoRenderRows.reduce((acc, cur) => ({ ...acc, [cur.id]: { mode: GridRowModes.Edit } }), {})) + }, [rows]) + + const theme = useTheme() + + const borderRadius = theme.shape.borderRadius + + const hoverSx = useMemo(() => { + return { + ...collapseTableSx(borderRadius) + } + }, [borderRadius]) + + const handleOpenMoreSetting = useCallback( + (params: GridRowParams) => { + setOpenMoreSettingDialog(true) + const rowMeta = tableApiRef.current.state.editRows[params.row.id] + const row = Object.keys(rowMeta).reduce((acc, key) => Object.assign(acc, { [key]: rowMeta[key].value }), {}) + setCurrentMoreSettingParam({ ...params.row, ...row }) + }, + [tableApiRef] + ) + + const handleCloseMoreSetting = useCallback(() => { + setOpenMoreSettingDialog(false) + setCurrentMoreSettingParam(null) + }, []) + + const handleMoreSettingChange = useCallback( + ({ id, param }: { id: string; param: Partial }) => { + const renderRow = renderRows.find((row) => row.id === id) + if (!renderRow) return + const row = renderRow.__raw__! + Object.assign(row.paramAttr, param) + setRows([...rows]) + setDirty(true) + handleCloseMoreSetting() + }, + [handleCloseMoreSetting, renderRows, rows] + ) + + const handleRowDelete = useCallback( + (params: GridRowParams) => { + const { id, parent } = params.row + const parentChildList = parent?.childList ?? rows + const index = parentChildList.findIndex((i) => i.id === id) + parentChildList.splice(index, 1) + setRows([...rows]) + setDirty(true) + }, + [rows] + ) + + const [actionLength, setActionLength] = useState(0) + + useEffect(() => { + if (['JSON', 'XML'].includes(contentType)) { + setActionLength(4) + } else { + setActionLength(3) + } + }, [contentType]) + + const getActions = useCallback( + (params: GridRowParams) => { + const actions = [ + handleOpenMoreSetting(params)} /> + ] + const isXML = contentType === 'XML' + const isRoot = params.row.__globalIndex__ === 0 + if (['JSON', 'XML'].includes(contentType)) { + actions.unshift( + { + const newRow = EmptyRow() + const row = tableApiRef.current.state.editRows[params.row.id] + if (![ApiParamsType.array, ApiParamsType.object].includes(row.dataType.value)) { + tableApiRef.current.setEditCellValue({ + field: 'dataType', + id: params.row.id, + value: ApiParamsType.object + }) + params.row.dataType = ApiParamsType.object + } + if (params.row.__raw__ && !params.row.__raw__.childList?.length) { + params.row.__raw__.childList = [] + } + // FIXME: Temporary fix for the issue of unable to add child nodes to the root node + const rootRow = rows.find((row) => row.id === params.row.id) + if (rootRow) { + rootRow.childList?.unshift(newRow) + } else { + params.row?.__raw__?.childList?.unshift(newRow) + } + setRows([...rows]) + }} + /> + ) + if (!(isXML && isRoot)) { + actions.unshift( + { + const newRow = EmptyRow() + const currentLevelChildrenList = params.row.parent?.childList ?? rows + currentLevelChildrenList?.splice((params.row.__levelIndex__ || 0) + 1, 0, newRow) + setRows([...rows]) + }} + /> + ) + } + } + if (renderRows.length > 1) { + actions.push( handleRowDelete(params)} />) + } + return actions + }, + + [EmptyRow, tableApiRef, contentType, handleOpenMoreSetting, handleRowDelete, renderRows.length, rows] + ) + + const columns: (GridColDef | false)[] = [ + messageType === 'Header' && { + field: 'name', + headerName: '标签', + editable: true, + sortable:false, + renderEditCell: (params) => { + const options = RequestHeaders.map((option) => option.key) + return ( + } + renderOption={AutoCompleteOption} + onChange={(e, v) => { + tableApiRef.current.setEditCellValue({ id: params.row.id, field: 'name', value: v }, e) + const rowIndex = params.row.__globalIndex__ as number + if (renderRows.length === rowIndex + 1) { + const newRow = EmptyRow() + setRows((preRows)=>[...preRows,newRow]) + } + }} + /> + ) + }, + width: 200 + }, + messageType !== 'Header' && { + field: 'name', + headerName: '参数名', + width: 200, + editable: true, + sortable: false, + renderEditCell(params: GridRenderEditCellParams) { + const rowNode = params.rowNode + const id = params.row.id + const handleClick = (event: SyntheticEvent) => { + if (rowNode.type !== 'group') { + return + } + tableApiRef.current.setRowChildrenExpansion(id, !rowNode.childrenExpanded) + tableApiRef.current.setCellFocus(id, 'name') + event.stopPropagation() + } + return ( + + {showRootIndent && ( + + )} + { + const newValue = e.target.value as string + const rowIndex = params.row.__globalIndex__ + tableApiRef.current.setEditCellValue({ id: params.row.id, field: 'name', value: newValue }) + if ( + renderRows.length === rowIndex + 1 && + !(contentType === 'XML' && params.row.__globalIndex__ === 0) + ) { + const newRow = EmptyRow() + const currentLevelChildrenList = params.row.parent?.childList ?? rows + currentLevelChildrenList?.splice((params.row.__levelIndex__ || 0) + 1, 0, newRow) + setRows((preRows)=>[...preRows]) + } + }} + /> + + ) + } + }, + messageType === 'Body' && { + field: 'dataType', + headerName: '类型', + sortable: false, + width: 120, + type: 'singleSelect', + editable: true, + renderEditCell(params) { + const options = ApiParamsTypeOptions.map((option) => ({ + label: option.key, + value: option.value + })) + return ( + <> + +option.value === +params.row.dataType!)} + getOptionLabel={(option) => option.label} + renderInput={(inputParams) => ( + + )} + renderOption={AutoCompleteOption} + onChange={(e, v) => { + const newValue = v?.value + params.api.setEditCellValue({ id: params.id, field: params.field, value: newValue }, e) + setDirty(true) + }} + /> + + ) + } + }, + { + field: 'isRequired', + headerName: '必需', + headerAlign: 'left', + sortable: false, + type: 'boolean', + editable: true, + renderHeader() { + return ( + + + 必需 + + ) + }, + width: 120, + renderEditCell: (params) => ( + + { + handleIsRequiredChange(checked, params.row.id) + setDirty(true) + }} + /> + + ) + }, + { + field: 'description', + headerName: '描述', + sortable: false, + flex: 1, + minWidth: 200, + editable: true, + renderEditCell(params) { + return ( + { + const newValue = e.target.value + params.api.setEditCellValue({ id: params.id, field: params.field, value: newValue }, e) + setDirty(true) + }} + sx={{ + '&.MuiTextField-root input': { + paddingLeft: theme.spacing(1), + paddingRight: theme.spacing(1) + } + }} + placeholder='描述' + /> + ) + } + }, + { + field: 'paramAttr', + sortable: false, + headerName: '示例', + flex: 1, + minWidth: 200, + editable: true, + renderEditCell(params) { + return ( + { + const newValue = e.target.value + params.api.setEditCellValue( + { id: params.id, field: params.field, value: { ...params.row.paramAttr, example: newValue } }, + e + ) + setDirty(true) + }} + sx={{ + '&.MuiTextField-root input': { + paddingLeft: theme.spacing(1), + paddingRight: theme.spacing(1) + } + }} + placeholder='示例' + /> + ) + } + }, + { + field: 'actions', + type: 'actions', + resizable: false, + sortable: false, + width: getActionColWidth(actionLength), + hideable: true, + align: 'left', + getActions + } + ] + + const handleIsRequiredChange = (checked: boolean, id: string) => { + tableApiRef.current.setEditCellValue({ id, field: 'isRequired', value: checked }) + updateSelectAll() + } + + const handleSelectAllChange = (_event: ChangeEvent, checked: boolean) => { + tableApiRef.current.getAllRowIds().forEach((id) => { + tableApiRef.current.setEditCellValue({ id, field: 'isRequired', value: checked }) + }) + setSelectAll(checked ? 'checked' : 'unchecked') + } + + const moreSettingHiddenConfig = useMoreSettingHiddenConfig({ + param: currentMoreSettingParam as RenderMessageBody, + messageType: messageType as MessageType, + readOnly: Boolean(isMoreSettingReadOnly) + }) + + const getEditMeta = () => { + const editMeta = tableApiRef.current.state.editRows + traverse( + rows, + (node: BodyParamsType, index: number) => { + const editRowMeta = editMeta[node.id] + node.orderNo = index + if (editRowMeta) { + Object.keys(editRowMeta).forEach((key) => { + const value = editRowMeta[key]?.value + if (!isNil(value)) { + node[key as keyof BodyParamsType] = value + } + if (key === 'isRequired') { + node[key as keyof BodyParamsType] = +value + } + }) + } + }, + 'childList' + ) + return rows.filter((row) => row.name) as BodyParamsType[] + } + + useImperativeHandle(apiRef, () => ({ + getEditMeta + })) + + return ( + + col) as GridColDef[]} + defaultGroupingExpansionDepth={-1} + pagination={false} + hideFooter + treeData + disableColumnMenu={true} + disableColumnReorder={true} + disableColumnPinning={true} + disableColumnSorting={true} + getTreeDataPath={(row) => row.path!} + groupingColDef={groupingColDef} + autosizeOptions={{ + expand: true, + includeHeaders: false + }} + loading={loading} + slots={{ + loadingOverlay: LinearProgress + }} + /> + {/* Dialog */} + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiManager/components/UriInput/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiManager/components/UriInput/index.tsx new file mode 100644 index 00000000..6a665187 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiManager/components/UriInput/index.tsx @@ -0,0 +1,112 @@ +import { InputAdornment, TextField, Select, MenuItem, Divider, SelectChangeEvent, Typography, Box } from '@mui/material' +import { SyntheticEvent } from 'react' +import {ParseCurlResult} from "@common/const/api-detail"; +import {HTTPMethod, RequestMethod} from "../../../RequestMethod"; +import {ParseCurl} from "@common/utils/curl.ts"; + +interface UriInputProps { + inputValue?: string + onInputChange?: (value: string) => void + selectValue?: HTTPMethod + onSelectChange?: (value: HTTPMethod) => void + onCURLPaste?: (cURL: ParseCurlResult) => void + onTest?: () => void +} + +export function UriInput({ + inputValue, + onInputChange, + selectValue, + onSelectChange, + onCURLPaste, + onTest +}: UriInputProps) { + + + const handleSelectChange = (event: SelectChangeEvent) => { + onSelectChange?.(event.target.value as HTTPMethod) + } + + const handleInputChange = (event: SyntheticEvent) => { + onInputChange?.((event.target as HTMLInputElement).value) + } + + const handlePaste = (event: React.ClipboardEvent) => { + const clipboardData = event.clipboardData + const pastedData = clipboardData.getData('text') + const cURL = new ParseCurl(pastedData) + onCURLPaste?.(cURL.getParseResult()) + } + + const httpMethods = [ + HTTPMethod.POST, + HTTPMethod.GET, + HTTPMethod.PUT, + HTTPMethod.DELETE, + HTTPMethod.HEAD, + HTTPMethod.OPTIONS, + HTTPMethod.PATCH + ] + + return ( + { + if (event.key === 'Enter') { + onTest?.() + } + }} + onPaste={handlePaste} + autoComplete="off" + InputProps={{ + startAdornment: ( + + + {/*{selectedEnv && selectedEnv.hostUri ? (*/} + {/* */} + {/* */} + {/* {selectedEnv.hostUri}*/} + {/* */} + {/*) : null}*/} + + + ) + }} + /> + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiManager/constants.ts b/frontend/packages/common/src/components/postcat/api/ApiManager/constants.ts new file mode 100644 index 00000000..abc9bbcb --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiManager/constants.ts @@ -0,0 +1,71 @@ +import {generateId} from "@common/utils/postcat.tsx"; +import { + ApiBodyType, + BodyParamsType, + HeaderParamsType, + QueryParamsType, ResponseList, + RestParamsType +} from "@common/const/api-detail"; +import {Protocol} from "../RequestMethod"; + +type SafeAny = unknown +type Timestamp = number + +declare interface HttpRequestMessage { + bodyParams: BodyParamsType[] + restParams: RestParamsType[] + queryParams: QueryParamsType[] + headerParams: HeaderParamsType[] +} + +declare interface ApiAttrInfo { + requestMethod: number + authInfo: SafeAny + authType: 'inherited' + contentType: ApiBodyType + createTime: Timestamp + createUserId: number + id: number + updateTime: number + updateUserId: number +} + +declare interface ApiRequest { + uri: string + apiAttrInfo: ApiAttrInfo + requestParams: HttpRequestMessage + responseList: ResponseList[] + apiCaseUuid: string + apiUuid: string + createTime: Timestamp + createUserId: number + id: number + name: string + projectId: number + groupId: number + projectUuid: string + protocol: Protocol + updateTime: Timestamp + updateUserId: number + /** EDIT */ + updateApiAttr?: 1 + updateRequestParams?: 1 + updateResponseList?: 1 +} + +export function getDefaultApiInfo(): ApiRequest { + return { + apiAttrInfo: {}, + apiUuid: generateId(), + name: '', + uri: '', + protocol: 0, + requestParams: { + bodyParams: [], + headerParams: [], + queryParams: [], + restParams: [] + }, + responseList: [{}] + } as unknown as ApiRequest +} \ No newline at end of file diff --git a/frontend/packages/common/src/components/postcat/api/ApiPreview/components/ApiBasicInfoDisplay/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiPreview/components/ApiBasicInfoDisplay/index.tsx new file mode 100644 index 00000000..fe721aec --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiPreview/components/ApiBasicInfoDisplay/index.tsx @@ -0,0 +1,60 @@ + +import { Box, Chip, Stack, Typography, Skeleton } from '@mui/material' +import {HTTPMethod, Protocol,RequestMethod} from "../../../RequestMethod"; +import {Clipboard} from "../../../Clipboard" + +interface ApiBasicInfoDisplayProps { + apiName: string + protocol: Protocol + method: HTTPMethod + uri: string + loading?: boolean +} + +export default function ApiBasicInfoDisplay(props: Partial) { + const { apiName, protocol, method, uri = '', loading = false } = props + + // const selectedEnv = useEnvStore((state) => state.selectedEnv) + + const fontHeight = 16 + if (loading) { + return ( + + + + + + + + + + + + ) + } + + return ( + + + + + + + {/*{selectedEnv ? selectedEnv.hostUri : ''}*/} + {uri} + + + + +

{apiName}

+
+ ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiPreview/components/ApiMatch/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiPreview/components/ApiMatch/index.tsx new file mode 100644 index 00000000..4b6e8aba --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiPreview/components/ApiMatch/index.tsx @@ -0,0 +1,78 @@ + +import { Box, useTheme } from "@mui/material" +import { DataGridPro, GridColDef } from '@mui/x-data-grid-pro' +import { useMemo } from 'react' +import {collapseTableSx, previewTableHoverSx} from "../../../PreviewTable"; +import {Collapse} from "../../../Collapse"; +import { MatchPositionEnum, MatchTypeEnum } from "@core/const/system/const"; +import { MatchItem } from "@common/const/type"; + +interface ApiMatchProps { + rows?: MatchItem[] + loading?: boolean + validating?: boolean + title: string | React.ReactNode +} + +export default function ApiMatch({ rows = [], title, loading = false }: ApiMatchProps) { + const theme = useTheme() + + const borderRadius = theme.shape.borderRadius + + const hoverSx = useMemo(() => { + return { + ...previewTableHoverSx(), + ...collapseTableSx(borderRadius) + } + }, [borderRadius]) + + const columns: GridColDef[] = [ + { + field: 'key', + headerName: '参数名', + hideable: false, + width:200 + }, + { + field: 'position', + headerName: '参数位置', + valueGetter: (params) => MatchPositionEnum[params.row.position], + width:160 + }, + { + field: 'matchType', + headerName: '匹配类型', + valueGetter: (params) => MatchTypeEnum[params.row.matchType], + width:160 + }, + { + field: 'pattern', + headerName: '参数值', + flex:1 + } + ] + + return ( + + + + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiPreview/components/ApiProxy/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiPreview/components/ApiProxy/index.tsx new file mode 100644 index 00000000..d16cf0b5 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiPreview/components/ApiProxy/index.tsx @@ -0,0 +1,117 @@ +import { useTheme, Box } from "@mui/material" +import { GridColDef, DataGridPro } from "@mui/x-data-grid-pro" +import { Descriptions } from "antd" +import { useState, useMemo, useEffect } from "react" +import { SystemApiProxyType, ProxyHeaderItem } from "@core/const/system/type" +import { previewTableHoverSx, collapseTableSx } from "../../../PreviewTable" +import { RenderMessageBody } from "../MessageBody" +import { Collapse } from "../../../Collapse" + +interface HeaderFieldsProps { + proxyInfo:SystemApiProxyType + loading?: boolean + validating?: boolean + title: string + onMoreSettingChange?: (row: RenderMessageBody) => void +} + +export default function ApiProxy({ proxyInfo, title, loading = false, onMoreSettingChange }: HeaderFieldsProps) { + const theme = useTheme() + const [rows,setRows] = useState<[]>([]) + const borderRadius = theme.shape.borderRadius + + const hoverSx = useMemo(() => { + return { + ...previewTableHoverSx(), + ...collapseTableSx(borderRadius) + } + }, [borderRadius]) + + const columns: GridColDef[] = [ + { + field: 'key', + headerName: '参数名', + width: 200, + hideable: false + }, + { + field: 'optType', + headerName: '操作类型', + valueGetter: (params) => params.row.optType === 'ADD'?'新增或修改':'删除', + width: 200 + }, + { + field: 'value', + headerName: '匹配参数值', + flex: 1 + }, + ] + + const getBasicInfo = useMemo(() => { + return [ + { + key: 'path', + label: '转发上游路径', + children: proxyInfo?.path, + style: {paddingBottom: '10px'}, + }, + { + key: 'timeout', + label: '请求超时时间', + children: proxyInfo?.timeout, + style: {paddingBottom: '10px'}, + }, + // { + // key: 'upstream', + // label: '绑定上游服务', + // children: proxyInfo?.upstream.name, + // style: {paddingBottom: '10px'}, + // }, + { + key: 'retry', + label: '重试时间', + children: proxyInfo?.retry, + style: {paddingBottom: '10px'}, + }, + ...(proxyInfo.headers.length > 0 ? [{ + key: 'headers', + label: '转发上游请求头', + children: '', + style: {paddingBottom: '10px'}, + }]:[]) + ]; + }, [proxyInfo]); + + useEffect(() => { + setRows(proxyInfo?.headers || []) + }, [proxyInfo]); + + return ( + + + 0 ? 'border-0 border-b border-solid border-b-BORDER': ''} `} title="" items={getBasicInfo} column={2} labelStyle={{width:'120px',justifyContent:'flex-end',fontWeight:'bold'}} contentStyle={{color:'#333'}}/> + + {proxyInfo?.headers?.length > 0 && + + } + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiPreview/components/HeaderFields/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiPreview/components/HeaderFields/index.tsx new file mode 100644 index 00000000..e04f80a5 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiPreview/components/HeaderFields/index.tsx @@ -0,0 +1,96 @@ +import { Box, LinearProgress, useTheme } from "@mui/material" +import { DataGridPro, GridColDef } from '@mui/x-data-grid-pro' +import { useMemo } from 'react' +import { RenderMessageBody } from '../MessageBody' +import {HeaderParamsType} from "@common/const/api-detail"; +import {collapseTableSx, PreviewGridActionsCellItem, previewTableHoverSx} from "../../../PreviewTable"; +import {Collapse} from "../../../Collapse"; + +interface HeaderFieldsProps { + rows?: HeaderParamsType[] + loading?: boolean + validating?: boolean + title: string + onMoreSettingChange?: (row: RenderMessageBody) => void +} + +export default function HeaderFields({ rows = [], title, loading = false, onMoreSettingChange }: HeaderFieldsProps) { + const theme = useTheme() + + const borderRadius = theme.shape.borderRadius + + const hoverSx = useMemo(() => { + return { + ...previewTableHoverSx(), + ...collapseTableSx(borderRadius) + } + }, [borderRadius]) + + const columns: GridColDef[] = [ + { + field: 'name', + headerName: '标签', + width: 200, + hideable: false + }, + { + field: 'isRequired', + headerName: '必需', + sortable: false, + valueGetter: (params) => Boolean(params.row.isRequired), + type: 'boolean', + width: 200 + }, + { + field: 'description', + headerName: '描述', + flex: 1 + }, + { + field: 'actions', + // renderHeader: () => , + type: 'actions', + resizable: false, + sortable: false, + width: 40, + hideable: true, + getActions: (params) => [ + onMoreSettingChange?.(params.row as unknown as RenderMessageBody)} + /> + ] + } + ] + + return ( + + + + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiPreview/components/MessageBody/components/Binary.tsx b/frontend/packages/common/src/components/postcat/api/ApiPreview/components/MessageBody/components/Binary.tsx new file mode 100644 index 00000000..6e310e2b --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiPreview/components/MessageBody/components/Binary.tsx @@ -0,0 +1,14 @@ +import { TextField } from '@mui/material' + +export function PreviewBodyBinary({ value }: { value: string;}) { + return ( + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiPreview/components/MessageBody/components/Raw.tsx b/frontend/packages/common/src/components/postcat/api/ApiPreview/components/MessageBody/components/Raw.tsx new file mode 100644 index 00000000..3b7acb14 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiPreview/components/MessageBody/components/Raw.tsx @@ -0,0 +1,8 @@ +import {Codebox} from "../../../../Codebox"; + +interface RequestBodyRawProps { + value: string +} +export function PreviewBodyRaw({ value }: RequestBodyRawProps) { + return +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiPreview/components/MessageBody/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiPreview/components/MessageBody/index.tsx new file mode 100644 index 00000000..af02da03 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiPreview/components/MessageBody/index.tsx @@ -0,0 +1,133 @@ +import { Box, LinearProgress, useTheme } from '@mui/material' +import { DataGridPro, GridColDef, useGridApiRef } from '@mui/x-data-grid-pro' +import { useEffect, useMemo } from 'react' +import {ApiBodyType, ApiParamsType, BodyParamsType} from "@common/const/api-detail"; +import {collapseTableSx, PreviewGridActionsCellItem, previewTableHoverSx} from "../../../PreviewTable"; +import {Collapse} from "../../../Collapse"; +import { PreviewBodyBinary } from './components/Binary'; +import { PreviewBodyRaw } from './components/Raw'; + +export interface RenderMessageBody extends BodyParamsType { + path: string[] +} + +interface MessageBodyProps { + rows?: RenderMessageBody[] + loading?: boolean + validating?: boolean + contentType?: ApiBodyType + title: string + onMoreSettingChange?: (row: RenderMessageBody) => void +} + +export default function MessageBodyComponent({ + rows = [], + contentType, + title, + validating, + loading = false, + onMoreSettingChange +}: MessageBodyProps) { + const apiRef = useGridApiRef() + const theme = useTheme() + + const borderRadius = theme.shape.borderRadius + + const hoverSx = useMemo(() => { + return { + ...previewTableHoverSx(), + ...collapseTableSx(borderRadius) + } + }, [borderRadius]) + + + useEffect(() => { + rows.forEach((row) => { + row?.childList?.length && apiRef.current.setRowChildrenExpansion(row.id, true) + }) + }, [apiRef, rows, validating]) + + const columns: GridColDef[] = [ + { + field: 'dataType', + headerName: '类型', + valueGetter: (params) => ApiParamsType[params.row.dataType as ApiParamsType], + width: 80 + }, + { + field: 'isRequired', + headerName: '必需', + sortable: false, + valueGetter: (params) => Boolean(params.row.isRequired), + type: 'boolean', + width: 200 + }, + { + field: 'description', + headerName: '描述', + flex: 1 + }, + { + field: 'paramAttr', + headerName: '示例', + valueGetter: (params) => params.row.paramAttr?.example, + flex: 1 + }, + { + field: 'actions', + type: 'actions', + resizable: false, + sortable: false, + width: 40, + hideable: true, + getActions: (params) => [ + onMoreSettingChange?.(params.row)} + /> + ] + } + ] + + return ( + + + {contentType !== ApiBodyType.Binary && contentType !== ApiBodyType.Raw && row.path} + hideFooter + disableColumnMenu={true} + disableColumnReorder={true} + disableColumnPinning={true} + autosizeOptions={{ + expand: true, + includeHeaders: false + }} + loading={loading} + slots={{ + loadingOverlay: LinearProgress + }} + />} + { contentType ===ApiBodyType.Binary && } + { contentType === ApiBodyType.Raw && } + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiPreview/components/QueryFields/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiPreview/components/QueryFields/index.tsx new file mode 100644 index 00000000..b7aaf4a2 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiPreview/components/QueryFields/index.tsx @@ -0,0 +1,97 @@ + +import { Box, LinearProgress, useTheme } from '@mui/material' +import { DataGridPro, GridColDef } from '@mui/x-data-grid-pro' +import { useMemo } from 'react' +import { RenderMessageBody } from '../MessageBody' +import {QueryParamsType} from "@common/const/api-detail"; +import {collapseTableSx, PreviewGridActionsCellItem, previewTableHoverSx} from "../../../PreviewTable"; +import {Collapse} from "../../../Collapse"; + +interface QueryFieldsProps { + rows?: QueryParamsType[] + loading?: boolean + validating?: boolean + title: string + onMoreSettingChange?: (row: RenderMessageBody) => void +} + +export default function QueryFields({ rows = [], title, loading = false, onMoreSettingChange }: QueryFieldsProps) { + const theme = useTheme() + + const borderRadius = theme.shape.borderRadius + + const hoverSx = useMemo(() => { + return { + ...previewTableHoverSx(), + ...collapseTableSx(borderRadius) + } + }, [borderRadius]) + + const columns: GridColDef[] = [ + { + field: 'name', + headerName: '参数名', + width: 200, + hideable: false + }, + { + field: 'isRequired', + headerName: '必需', + sortable: false, + valueGetter: (params) => Boolean(params.row.isRequired), + type: 'boolean', + width: 200 + }, + { + field: 'description', + headerName: '描述', + flex: 1 + }, + { + field: 'actions', + // renderHeader: () => , + type: 'actions', + resizable: false, + sortable: false, + width: 40, + hideable: true, + getActions: (params) => [ + onMoreSettingChange?.(params.row as unknown as RenderMessageBody)} + /> + ] + } + ] + + return ( + + + + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/ImportMessage/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/ImportMessage/index.tsx new file mode 100644 index 00000000..90c3e19b --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/ImportMessage/index.tsx @@ -0,0 +1,104 @@ +import { Box, Button, DialogContent, Paper, Typography, useTheme } from '@mui/material' +import { useEffect, useMemo, useState } from 'react' +import { IconButton } from '../../../../IconButton' +import {BaseDialog} from "../../../../Dialog"; +import {Icon} from "../../../../Icon"; +import {Codebox} from "../../../../Codebox"; + +export type ImportMessageChangeType = 'replace-all' | 'insert-end' | 'replace-changed' + +export type ImportMessageOption = { + key: string + value: string +} + +export type ImportMessageType = 'query' | 'form-data' | 'header' + +interface ImportMessageDialogProps { + type: ImportMessageType + onChange?: (changeType: ImportMessageChangeType, data: ImportMessageOption[]) => void +} + +export function ImportMessage({ type, onChange }: ImportMessageDialogProps) { + const [open, setOpen] = useState(false) + const [code, setCode] = useState('') + const theme = useTheme() + + const example = useMemo(() => { + if (type === 'query') { + return `/api?name=Jack&age=18` + } + if (type === 'form-data') { + return 'name: Jack\nage: 18' + } + if (type === 'header') return 'headerName: headerValue\nheaderName2: headerValue2' + }, [type]) + + useEffect(() => { + setCode('') + }, [open]) + + const handleChange = (changeType: ImportMessageChangeType) => { + setOpen(false) + if (!code) return + if (['form-data', 'header'].includes(type)) { + const data = code.split('\n').map((line) => { + const [key, value] = line.split(':') + return { key: key?.trim(), value: value?.trim() } + }) + onChange?.(changeType, data) + return + } + if (['query'].includes(type)) { + const data = code + .split('?')[1] + .split('&') + .map((line) => { + const [key, value] = line.split('=') + return { key: key.trim(), value: value.trim() } + }) + onChange?.(changeType, data) + return + } + } + + return ( + <> + setOpen(true)} variant="outlined"> + 导入 + + setOpen(false)} actionRender={null} title={`Import ${type}`}> + + + + + + + 导入格式 + + +
{example}
+
+
+
+ +
+
+ + + + + + +
+ + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/TestBody/Binary.tsx b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/TestBody/Binary.tsx new file mode 100644 index 00000000..220c497c --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/TestBody/Binary.tsx @@ -0,0 +1,10 @@ +import { Upload } from '../../../../Upload' + +interface BinaryProps { + value: File | null + onChange: (value: File | null) => void +} + +export function Binary({ value, onChange }: BinaryProps) { + return +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/TestBody/FormData.tsx b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/TestBody/FormData.tsx new file mode 100644 index 00000000..2274db9c --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/TestBody/FormData.tsx @@ -0,0 +1,34 @@ +import { useContext, useEffect, useImperativeHandle, useRef, useState } from 'react' +import {BodyParamsType} from "@common/const/api-detail"; +import {TestMessageDataGrid, TestMessageDataGridApi} from "../../TestMessageDataGrid"; + +interface FormDataProps { + apiRef: React.RefObject> +} + +export function FormData({ apiRef }: FormDataProps) { + const [apiBody, setApiBody] = useState(null) + // const { apiInfo, loaded } = useContext>(ApiTabContext) + + const formDataApiRef = useRef(null) + + // useEffect(() => { + // if (loaded && (apiInfo || apiInfo === null)) { + // setApiBody(apiInfo?.requestParams.bodyParams || []) + // } + // }, [apiInfo, loaded]) + + useImperativeHandle(apiRef, () => ({ + getEditMeta: () => { + return formDataApiRef.current?.getEditMeta() || [] + }, + importData(changeType, data) { + return formDataApiRef.current?.importData(changeType, data) + }, + updateRows(data: BodyParamsType[]) { + return formDataApiRef.current?.updateRows(data) + } + })) + + return +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/TestBody/Raw.tsx b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/TestBody/Raw.tsx new file mode 100644 index 00000000..fad24d21 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/TestBody/Raw.tsx @@ -0,0 +1,10 @@ +import {Codebox} from "../../../../Codebox"; + +interface RawProps { + value: string + onChange: (value: string) => void +} + +export function Raw({ value, onChange }: RawProps) { + return +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/TestBody/const.ts b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/TestBody/const.ts new file mode 100644 index 00000000..7c8d7f3c --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/TestBody/const.ts @@ -0,0 +1,45 @@ +export type ContentType = "text/plain" | "application/json" | "application/xml" | "text/html" | "application/javascript" | 'application/x-www-form-urlencoded' | 'multipart/form-data' + +export type MimeTypeKey = 'Text' | 'JSON' | 'XML' | 'HTML' | 'JavaScript' + +export type FormContentTypeKey = 'x-www-form-urlencoded' | 'multipart/form-data' + +export const MimeTypes: { + title: MimeTypeKey, + value: ContentType +}[] = [ + { + title: 'Text', + value: 'text/plain' + }, + { + title: 'JSON', + value: 'application/json' + }, + { + title: 'XML', + value: 'application/xml' + }, + { + title: 'HTML', + value: 'text/html' + }, + { + title: 'JavaScript', + value: 'application/javascript' + } + ] + +export const FormContentTypes: { + title: FormContentTypeKey, + value: ContentType +}[] = [ + { + title: 'x-www-form-urlencoded', + value: 'application/x-www-form-urlencoded' + }, + { + title: 'multipart/form-data', + value: 'multipart/form-data' + } + ] \ No newline at end of file diff --git a/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/TestBody/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/TestBody/index.tsx new file mode 100644 index 00000000..320ba3bd --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/TestBody/index.tsx @@ -0,0 +1,242 @@ +import { + Box, + Divider, + FormControl, + FormControlLabel, + MenuItem, + Radio, + RadioGroup, + Select, + SelectChangeEvent, + SxProps, + Theme, + Typography +} from '@mui/material' +import { ChangeEvent, useEffect, useImperativeHandle, useRef, useState } from 'react' +import { FormData } from './FormData' +import { Raw } from './Raw' +import { Binary } from './Binary' +import { ContentType, FormContentTypes, MimeTypes } from './const' +import { ImportMessage, ImportMessageChangeType, ImportMessageOption } from '../ImportMessage' +import {ApiBodyType, BodyParamsType, TestApiBodyType} from "@common/const/api-detail"; +import {mapContentTypeToApiBodyType} from "@common/utils/postcat.tsx"; +import {ParseCurlResult} from "@common/utils/curl.ts"; +import {TestMessageDataGridApi} from "../../TestMessageDataGrid"; + +export interface TestBodyApi { + getBodyMeta: () => { + apiBodyType: TestApiBodyType + contentType: ContentType | null + data?: string | File | Partial[] | null + } + updateRequestBodyWithCurlInfo: (cURLResult: ParseCurlResult) => void + updateRequestBody: (data: TestBodyType) => void +} + +interface TestBodyProps { + bodyApiRef?: React.RefObject + onContentTypeChange?: (contentType: ContentType) => void +} + +export interface TestBodyType { + apiBodyType: TestApiBodyType + contentType: ContentType | null + data?: string | File | Partial[] | null +} + +declare type Optional = Omit & Partial> + +export function TestBody({ bodyApiRef, onContentTypeChange }: TestBodyProps) { + const [apiBodyTypeValue, setApiBodyTypeValue] = useState(ApiBodyType.FormData) + + const [contentType, setContentType] = useState(null) + const [raw, setRaw] = useState('') + const [binary, setBinary] = useState(null) + + const formDataApiRef = useRef(null) + + useEffect(() => { + if (apiBodyTypeValue === ApiBodyType.FormData) { + setContentType(FormContentTypes[0].value) + } + if (apiBodyTypeValue === ApiBodyType.Raw) { + setContentType(MimeTypes[0].value) + } + }, [apiBodyTypeValue]) + + useEffect(() => { + onContentTypeChange?.(contentType as ContentType) + }, [contentType, onContentTypeChange]) + + const getBodyMeta = () => { + const result: Optional = { + contentType + } + if (apiBodyTypeValue === ApiBodyType.FormData) { + result.data = formDataApiRef.current?.getEditMeta() || [] + result.apiBodyType = ApiBodyType.FormData + } + if (apiBodyTypeValue === ApiBodyType.Raw) { + result.data = raw + result.apiBodyType = ApiBodyType.Raw + } + if (apiBodyTypeValue === ApiBodyType.Binary) { + result.data = binary + result.apiBodyType = ApiBodyType.Binary + } + return result as TestBodyType + } + + const updateRequestBodyWithCurlInfo = (cURLResult: ParseCurlResult) => { + const contentType = cURLResult.contentType as ContentType + const apiBodyType = mapContentTypeToApiBodyType(contentType) + try { + if (apiBodyType === ApiBodyType.FormData) { + const requestParams: unknown = cURLResult?.requestParams || {} + const messageBodyList = Object.keys(requestParams).map((key) => ({ + name: key, + paramAttr: { + example: requestParams[key] + } + })) as BodyParamsType[] + formDataApiRef.current?.updateRows(messageBodyList) + setApiBodyTypeValue(ApiBodyType.FormData) + } else { + setRaw(cURLResult?.body || '') + formDataApiRef.current?.updateRows([]) + setApiBodyTypeValue(ApiBodyType.Raw) + } + } catch (err) { + console.warn(err) + } + } + + const updateRequestBody = (data: TestBodyType) => { + setApiBodyTypeValue(data.apiBodyType) + setContentType(data.contentType) + if (data.apiBodyType === ApiBodyType.FormData) { + formDataApiRef.current?.updateRows(data.data as BodyParamsType[]) + } + if (data.apiBodyType === ApiBodyType.Raw) { + setRaw(data.data as string) + } + } + + useImperativeHandle(bodyApiRef, () => ({ + getBodyMeta, + updateRequestBodyWithCurlInfo, + updateRequestBody + })) + + const handleImportChange = (changeType: ImportMessageChangeType, data: ImportMessageOption[]) => { + formDataApiRef.current?.importData(changeType, data) + } + + const apiBodyTypeList: { + key: 'Form-Data' | 'Raw' | 'Binary' + value: TestApiBodyType + element: JSX.Element + }[] = [ + { + key: 'Form-Data', + value: ApiBodyType.FormData, + element: + }, + { + key: 'Raw', + value: ApiBodyType.Raw, + element: + }, + { + key: 'Binary', + value: ApiBodyType.Binary, + element: + } + ] + + const handleApiBodyTypeValueChange = (event: ChangeEvent) => { + setApiBodyTypeValue(+(event.target as HTMLInputElement).value) + } + + const handleContentTypeChange = (event: SelectChangeEvent) => { + setContentType(event.target.value as ContentType) + } + + const optionSx: SxProps = { + fontSize: '12px', + height: '24px' + } + + return ( + + + + + {apiBodyTypeList.map((apiBodyType) => ( + } + label={apiBodyType.key} + /> + ))} + + + + + {[ApiBodyType.Raw, ApiBodyType.FormData].includes(apiBodyTypeValue) ? ( + <> + Content-Type: + + + ) : null} + {apiBodyTypeValue === ApiBodyType.FormData ? ( + <> + + + + + + ) : null} + + + + {apiBodyTypeList.map((apiBodyType) => ( + + ))} + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/index.tsx new file mode 100644 index 00000000..f07cfc9e --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiRequestTester/index.tsx @@ -0,0 +1,235 @@ +import { Box, Divider, Grow, Tab, Tabs, Typography, useTheme } from '@mui/material' +import { + RefObject, + SyntheticEvent, + useCallback, + useContext, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState +} from 'react' +import { TestBody, TestBodyApi, TestBodyType } from './TestBody' +import { ContentType } from './TestBody/const' +import {throttle} from 'lodash-es' +import { ImportMessage, ImportMessageChangeType, ImportMessageOption } from './ImportMessage' +import {ApiBodyType, ApiDetail, ParseCurlResult, TestApiBodyType} from "@common/const/api-detail"; +import {TestMessageDataGrid, TestMessageDataGridApi} from "../TestMessageDataGrid"; +import {Indicator} from "../../../../Indicator"; + +export interface ApiRequestTesterApi { + getEditMeta: () => { + headers: Partial[] | null + body?: { + apiBodyType: TestApiBodyType + contentType: ContentType | null + data?: string | File | Partial[] | null + } + query: Partial[] | null + rest: Partial[] | null + } + updateQueryDataGrid: (rows: ApiBodyType[]) => void + updateRestDataGrid: (rows: ApiBodyType[]) => void + updateHeaderDataGrid: (rows: ApiBodyType[]) => void + updateRequestBodyWithCurlInfo: (cURLResult: ParseCurlResult) => void + updateRequestBody: (data: TestBodyType) => void +} + +interface ApiRequestTesterProps { + apiRef: RefObject + onQueryChange: (query: ApiBodyType[]) => void + apiInfo:ApiDetail + loaded:boolean +} + +export function ApiRequestTester({ apiRef, onQueryChange ,apiInfo, loaded=true}: ApiRequestTesterProps) { + const [apiHeaders, setApiHeaders] = useState(null) + const [apiQuery, setApiQuery] = useState(null) + const [apiRest, setApiRest] = useState(null) + + const headersApiRef = useRef(null) + const queryApiRef = useRef(null) + const restApiRef = useRef(null) + const bodyApiRef = useRef(null) + + const getEditMeta = () => { + return { + headers: headersApiRef.current?.getEditMeta() || null, + body: bodyApiRef.current?.getBodyMeta(), + query: queryApiRef.current?.getEditMeta() || null, + rest: restApiRef.current?.getEditMeta() || null, + } + } + + const updateQueryDataGrid = (rows: ApiBodyType[]) => { + queryApiRef.current?.updateRows(rows) + } + + const updateRestDataGrid = (rows: ApiBodyType[]) => { + restApiRef.current?.updateRows(rows) + } + + const updateHeaderDataGrid = (rows: ApiBodyType[]) => { + headersApiRef.current?.updateRows(rows) + } + + const updateRequestBodyWithCurlInfo = (cURLResult: ParseCurlResult) => { + bodyApiRef.current?.updateRequestBodyWithCurlInfo(cURLResult) + } + + const updateRequestBody = (data: TestBodyType) => { + bodyApiRef.current?.updateRequestBody(data) + } + + useImperativeHandle(apiRef, () => ({ + getEditMeta, + updateQueryDataGrid, + updateRestDataGrid, + updateHeaderDataGrid, + updateRequestBodyWithCurlInfo, + updateRequestBody + })) + + useEffect(() => { + if ((apiInfo || apiInfo === null) && loaded) { + setApiHeaders(apiInfo?.requestParams?.headerParams || []) + setApiQuery(apiInfo?.requestParams?.queryParams || []) + setApiRest(apiInfo?.requestParams?.restParams || []) + } + }, [apiInfo, loaded]) + + const handleContentTypeChange = (contentType: ContentType) => { + headersApiRef.current?.updateContentType?.(contentType) + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + const handleQueryChange = useCallback( + throttle(() => { + const queryData = queryApiRef.current?.getEditMeta() || [] + onQueryChange?.(queryData as ApiBodyType[]) + }, 500), + [onQueryChange] + ) + + const handleImportChange = (changeType: ImportMessageChangeType, data: ImportMessageOption[]) => { + tabValue === '请求头' && headersApiRef.current?.importData(changeType, data) + tabValue === 'Query 参数' && queryApiRef.current?.importData(changeType, data) + } + + const tabs = [ + { + label: '请求头', + element: ( + + ), + dirty: false + }, + { + label: '请求体', + element: , + dirty: false + }, + { + label: 'Query 参数', + element: ( + + ), + dirty: false + }, + { + label: 'Rest 参数', + element: , + dirty: false + } + ] + + const tabHeight = '30px' + + const theme = useTheme() + + const [tabValue, setTabValue] = useState(tabs[0].label) + + const handleChange = (_event: SyntheticEvent, newValue: string) => { + setTabValue(newValue) + } + + const importMessageType: 'header' | 'query' = useMemo(() => { + return ({ Headers: 'header', Query: 'query' }[tabValue] || 'query') as 'header' | 'query' + }, [tabValue]) + + return ( + + + + {tabs.map((tab) => ( + + {tab.label} + + + + + + + } + sx={{ + textAlign: 'left', + padding: theme.spacing(1), + minWidth: 'auto', + minHeight: tabHeight, + fontSize:'14px' + }} + /> + ))} + + {['Headers', 'Query'].includes(tabValue) ? ( + + + + + ) : null} + + + {tabs.map((tab) => ( + + ))} + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiResponse/components/HeaderPreview.tsx b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiResponse/components/HeaderPreview.tsx new file mode 100644 index 00000000..2349afc9 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiResponse/components/HeaderPreview.tsx @@ -0,0 +1,22 @@ +import { Box, Typography, useTheme } from '@mui/material' + +export function HeaderPreview({ data }: { data: { key: string; value: string }[] }) { + const theme = useTheme() + + return ( + + {data.map((row) => ( + + + + {row.key} + + + + {row.value} + + + ))} + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiResponse/components/ResponseIndicator.tsx b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiResponse/components/ResponseIndicator.tsx new file mode 100644 index 00000000..31810380 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiResponse/components/ResponseIndicator.tsx @@ -0,0 +1,25 @@ +import { Box, Chip, Typography } from '@mui/material' +import {IconButton} from "../../../../IconButton"; +import {byteToString} from "@common/utils/postcat.tsx"; + +interface ResponseIndicatorProps { + statusCode: number + size: number + time: string + onDownload?: () => void +} + +export function ResponseIndicator({ statusCode, size, time, onDownload }: Partial) { + return statusCode ? ( + + {statusCode}} /> + + 大小: {byteToString(size || 0)} + + + 时间: {time} ms + + + + ) : null +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiResponse/components/body.tsx b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiResponse/components/body.tsx new file mode 100644 index 00000000..ef320fbc --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiResponse/components/body.tsx @@ -0,0 +1,13 @@ +import {Codebox} from "../../../../Codebox"; + +interface BodyProps { + data: string +} + +export function Body({ data }: BodyProps) { + return ( + <> + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiResponse/components/response.tsx b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiResponse/components/response.tsx new file mode 100644 index 00000000..3ff582e6 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiResponse/components/response.tsx @@ -0,0 +1,93 @@ +import { useEffect, useMemo, useRef } from 'react' +import { Box, Button, Typography } from '@mui/material' +import {ResponseContentType} from "@common/hooks/useTest.ts"; +import {Codebox,CodeboxApiRef} from "../../../../Codebox"; + +interface ResponseProps { + responseType: ResponseType + responseContentType: ResponseContentType + responseLength: number + data: string + uri: string + onDownload: () => void +} + +export function Response({ + data, + responseContentType, + responseType, + responseLength, + uri, + onDownload +}: Partial) { + const codeboxApiRef = useRef(null) + + const language = useMemo(() => { + const contentType: unknown = responseContentType + if (contentType?.includes('text/html')) return 'html' + if (contentType?.includes('application/json')) return 'json' + if (contentType?.includes('application/xml')) return 'xml' + if (contentType?.includes('application/javascript')) return 'javascript' + if (contentType?.includes('text/css')) return 'css' + if (contentType?.includes('text/plain')) return 'plaintext' + return 'plaintext' + }, [responseContentType]) + + const responsePreviewType: 'stream' | 'longText' | 'img' | 'default' = useMemo(() => { + const isImage = responseContentType?.startsWith('image') + const isLongData = (responseLength || 0) > 500000 + if (isImage) return 'img' + if (!isLongData) return 'default' + // @ts-ignore + if (isLongData && responseType === 'stream') return 'stream' + // @ts-ignore + if (isLongData && responseType === 'text') return 'longText' + return 'default' + }, [responseContentType, responseLength, responseType]) + + useEffect(() => { + if (responsePreviewType === 'default') { + codeboxApiRef.current?.formatCode() + } + }, [responsePreviewType, data]) + + return ( + <> + {responsePreviewType === 'img' ? ( + + response image + + ) : null} + {responsePreviewType === 'stream' ? ( + + Unable to preview non-text data types. Please + + the file and open it with an appropriate program. + + ) : null} + {responsePreviewType === 'longText' ? ( + + The response exceeds the size limit for preview. Please + + it for further review. + + ) : null} + {responsePreviewType === 'default' ? ( + <> + {/*
*/} + + ) : null} + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiResponse/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiResponse/index.tsx new file mode 100644 index 00000000..406d84d2 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiTest/components/ApiResponse/index.tsx @@ -0,0 +1,130 @@ +import { Box, Tab, Tabs, useTheme } from '@mui/material' +import { SyntheticEvent, useCallback, useMemo, useState } from 'react' +import { Response } from './components/response' +import { Body } from './components/body' +import { HeaderPreview } from './components/HeaderPreview' +import { ResponseIndicator } from './components/ResponseIndicator' +import {TestResponse} from "@common/hooks/useTest.ts"; +import {downloadFile} from "@common/utils/download.ts"; + +type TabType = 'Response' | 'Response Headers' | 'Body' | 'Request Headers' + +interface ApiResponseProps { + data: TestResponse | null +} + +export function ApiResponse({ data }: ApiResponseProps) { + const tabHeight = 30 + const theme = useTheme() + + const [tabValue, setTabValue] = useState('Response') + + const handleTabValueChange = (_evt: SyntheticEvent, value: TabType): void => { + setTabValue(value) + } + const response = data?.report.response + + const handleDownload = useCallback(() => { + const request = data?.report?.request + const response = data?.report.response + const report = data?.report + downloadFile({ + body: response?.body || '', + contentType: response?.contentType || 'raw', + filename: report?.blobFileName || 'test_response', + responseType: response?.responseType || 'text', + uri: request?.uri || '' + }) + }, [data]) + + const tabs = useMemo(() => { + const request = data?.report?.request + const response = data?.report.response + return [ + { + title: '响应', + name: 'Response', + hidden: false, + element: ( + + ) + }, + { + title: '响应头', + name: 'Response Headers', + hidden: !response?.headers.length, + element: + }, + { + title:'正文', + name: 'Body', + hidden: !request?.body.length, + element: + }, + { + title: '请求头', + name: 'Request Headers', + hidden: !request?.headers.length, + element: + } + ].filter((tab) => !tab.hidden) + }, [data, handleDownload]) + + return ( + + + + {tabs.map((tab, index) => ( + + ))} + + + + + {tabs.map((tab) => ( + + ))} + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiTest/components/TestControl/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiTest/components/TestControl/index.tsx new file mode 100644 index 00000000..6125233f --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiTest/components/TestControl/index.tsx @@ -0,0 +1,84 @@ +// import { TabRouteObject, useTabStore } from '@/stores/tab' +import { Button, Typography } from '@mui/material' +import { TouchRippleActions } from '@mui/material/ButtonBase/TouchRipple' +import { useEffect, useRef, useState } from 'react' +// import { useKey } from 'react-use' + +interface TestControlProps { + onTest: () => Promise | void + onAbort: () => void + loading: boolean +} + +export function TestControl({ onTest, onAbort, loading }: TestControlProps) { + const testRippleRef = useRef(null) + + const [loadingTime, setLoadingTime] = useState(0) + + // const { params: tabParams } = useContext>(TabContext) + + // const activeTab = useTabStore((state) => state.activeTab) + + // const theme = useTheme() + // useKey((event: KeyboardEvent) => { + // if (event.key === 'Enter') { + // event.preventDefault() + // triggerSaveRipple() + // //console.log('JK', activeTab?.params) + // //console.log('tabParams:', tabParams) + // // TODO 全等 + // onTest() + // } + // return true + // }) + + // const rippleDuration = theme.transitions.duration.short + + // const triggerSaveRipple = (): void => { + // if (testRippleRef.current) { + // const ripple = testRippleRef.current + // ripple.start() + // setTimeout(() => { + // ripple.stop() + // }, rippleDuration) + // } + // } + + useEffect(() => { + let timer: NodeJS.Timeout | null = null + + if (loading) { + setLoadingTime(0) + timer = setInterval(() => { + setLoadingTime((prevTime) => prevTime + 1) + }, 1000) + } else { + if (timer) clearInterval(timer) + } + + return () => { + if (timer) clearInterval(timer) + } + }, [loading]) + + + return ( + <> + {!loading ? ( + + ) : ( + + )} + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/ApiTest/components/TestMessageDataGrid/index.tsx b/frontend/packages/common/src/components/postcat/api/ApiTest/components/TestMessageDataGrid/index.tsx new file mode 100644 index 00000000..93da7881 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/ApiTest/components/TestMessageDataGrid/index.tsx @@ -0,0 +1,494 @@ + +import { Autocomplete, Box, LinearProgress, TextField, ThemeProvider, Tooltip, createTheme, useTheme } from '@mui/material' +import { + DataGridPro, GridCallbackDetails, + GridColDef, + GridRenderEditCellParams, + GridRowId, + GridRowModes, + GridRowModesModel, + GridRowParams, GridRowSelectionModel, + useGridApiRef +} from '@mui/x-data-grid-pro' +import { RefObject, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react' +import {Example,ApiParamsType, BodyParamsType, FileExample} from "@common/const/api-detail"; +import {ContentType} from "../ApiRequestTester/TestBody/const.ts"; +import {ImportMessageChangeType, ImportMessageOption} from "../ApiRequestTester/ImportMessage"; +import {generateNumberId, getActionColWidth, traverse} from "@common/utils/postcat.tsx"; +import {collapseTableSx} from "../../../PreviewTable"; +import {IconButton} from "../../../IconButton"; +import {RequestHeaders} from "../../../ApiManager/components/ApiRequestEditor/components/constants.ts"; +import { + AutoCompleteOption, + DataGridAutoCompleteProps, + DataGridTextFieldProps, EditableDataGridSx +} from "../../../ApiManager/components/EditableDataGrid"; +import {Icon} from "../../../Icon"; +import {ApiParamsTypeOptions} from "../../../ApiManager/components/ApiMessageBody/constants.ts"; +import {UploadButton} from "../../../UploadButton"; +import {isNil} from "lodash-es"; + +type SafeAny = unknown +export interface RenderBodyParamsType extends BodyParamsType { + __globalIndex__?: number + __levelIndex__?: number + __raw__?: BodyParamsType +} + +export type TestMessageType = 'Headers' | 'Body' | 'Query' | 'REST' + +export interface TestMessageDataGridApi { + getEditMeta: () => Partial[] + updateContentType?: (contentType: ContentType) => void + importData: (changeType: ImportMessageChangeType, data: ImportMessageOption[]) => void + updateRows: (rows: BodyParamsType[]) => void +} + +interface TestMessageDataGridProps { + onChange?: (rows: T[]) => void + onValueChange?: () => void + onNameChange?: () => void + initialRows?: T[] | null + onDirty?: () => void + loading?: boolean + messageType?: TestMessageType + apiRef?: RefObject + disabledContentType?: boolean +} + +export function TestMessageDataGrid(props: TestMessageDataGridProps) { + const { + onChange, + initialRows, + onDirty, + loading = false, + messageType, + apiRef, + disabledContentType = false, + onNameChange, + onValueChange + } = props + + const [rows, setRows] = useState<(BodyParamsType&{_checked?:boolean})[]>([]) + + const [renderRows, setRenderRows] = useState<(RenderBodyParamsType&{_checked?:boolean})[]>([]) + const [rowModesModel, setRowModesModel] = useState({}) + + const [dirty, setDirty] = useState(false) + + const [rowSelectionModel, setRowSelectionModel] = useState([]) + const EmptyRow = useCallback( + (val: string = ''): BodyParamsType => { + const id = generateNumberId() + const name = val + let dataType = null + if (messageType === 'Body') { + dataType = ApiParamsType.string + } + + return { + id, + name, + dataType, + _checked:true, + isRequired: 1, + description: '', + paramAttr: { + example: '' + }, + childList: [] + } as unknown as BodyParamsType + }, + [messageType] + ) + + useEffect(() => { + // setTimeout(()=>{ + // const element = document.querySelectorAll('.MuiDataGrid-main'); + // if(element?.length > 0){ + // for(const x of element){ + // x.childNodes[x.childNodes.length - 1 ].textContent === 'MUI X Missing license key' ? x.childNodes[x.childNodes.length - 1 ].textContent = '' :null + // } + // } + // },500) + }, []); + + useEffect(() => { + dirty && onDirty?.() + }, [dirty, onDirty]) + + const tableApiRef = useGridApiRef() + + useEffect(() => { + if (initialRows) { + const newRow = EmptyRow() + const updateRows = [...(initialRows||[]).map(x=>({...x,_checked:true})),newRow] + setRows(updateRows) + setRowSelectionModel(updateRows.map((row) => row.id)) + setDirty(false) + onChange?.(updateRows) + } + }, [EmptyRow, initialRows, onChange]) + + useEffect(() => { + const neoRenderRows = rows.map((row, rowIndex) => ({ ...row, __globalIndex__: rowIndex, __levelIndex__: rowIndex })) + setRenderRows(neoRenderRows) + setRowSelectionModel(neoRenderRows.filter(x=>x._checked).map((x)=>x.id)) + setRowModesModel(neoRenderRows.reduce((acc, cur) => ({ ...acc, [cur.id]: { mode: GridRowModes.Edit } }), {})) + }, [rows]) + + const theme = useTheme() + + const borderRadius = theme.shape.borderRadius + + const hoverSx = useMemo(() => { + return { + ...collapseTableSx(borderRadius) + } + }, [borderRadius]) + + const handleRowDelete = useCallback( + (params: GridRowParams) => { + const { id } = params.row + setRows(rows.filter((row) => row.id !== id)) + setDirty(true) + }, + [rows] + ) + + const getActions = useCallback( + (params: GridRowParams) => { + const actions = [] + if (renderRows.length > 1) { + actions.push( handleRowDelete(params)} />) + } + return actions + }, + [handleRowDelete, renderRows.length] + ) + + + const columns: (GridColDef | false)[] = [ + messageType === 'Headers' && { + field: 'name', + headerName: '标签', + editable: true, + sortable: false, + renderEditCell: (params) => { + const options = RequestHeaders.map((option) => ({ label: option.key })) + const contentTypeDisabled = params.value === 'Content-Type' && disabledContentType + return ( + + o.label)} + renderInput={(inputParams) => ( + + )} + renderOption={AutoCompleteOption} + onInputChange ={(e, v) => { + params.api.setEditCellValue({ id: params.id, field: params.field, value: v }, e) + const rowIndex = params.row.__globalIndex__ as number + if (renderRows.length === rowIndex + 1 && e.target?.value?.length === 1 ) { + const newRow = EmptyRow() + setRows(prevRow => [...prevRow, newRow]); + setRowSelectionModel(prevRowS => [...prevRowS, newRow.id]); + } + }} + /> + {contentTypeDisabled ? ( + + + + + + ) : null} + + ) + }, + width: 200 + }, + messageType !== 'Headers' && { + field: 'name', + headerName:'参数名', + width: 200, + editable: true, + sortable: false, + renderEditCell(params: GridRenderEditCellParams) { + return ( + + { + const newValue = e.target.value as string + const rowIndex = params.row.__globalIndex__ + tableApiRef.current.setEditCellValue({ id: params.row.id, field: 'name', value: newValue }) + if (renderRows.length === rowIndex + 1) { + const newRow = EmptyRow() + setRows([...rows, newRow]) + setRowSelectionModel([...rowSelectionModel, newRow.id]) + } + onNameChange?.() + }} + /> + + ) + } + }, + messageType === 'Body' && { + field: 'dataType', + headerName: '类型', + sortable: false, + width: 120, + type: 'singleSelect', + editable: true, + renderEditCell(params) { + const options = ApiParamsTypeOptions.filter((option) => ['string', 'file'].includes(option.key)).map( + (option) => ({ + label: option.key, + value: option.value + }) + ) + return ( + <> + +option.value === +params.row.dataType!)} + getOptionLabel={(option) => option.label} + renderInput={(inputParams) => ( + + )} + renderOption={AutoCompleteOption} + onChange={(e, v) => { + const newValue = v?.value + params.api.setEditCellValue({ id: params.id, field: params.field, value: newValue }, e) + setDirty(true) + }} + /> + + ) + } + }, + { + field: 'paramAttr', + sortable: false, + headerName: '参数值', + flex: 1, + minWidth: 200, + editable: true, + renderEditCell(params) { + const isFile = params.row.dataType === ApiParamsType.file + const value = params.row.paramAttr?.example as Example + if (isFile && value && !(value instanceof Array)) { + params.row.paramAttr.example = [] + } + if (!isFile && value && value instanceof Array) { + params.row.paramAttr.example = '' + } + return isFile ? ( + + { + params.api.setEditCellValue({ + id: params.id, + field: params.field, + value: { ...params.row.paramAttr, example: base64List } + }) + setDirty(true) + }} + /> + + ) : ( + { + const newValue = e.target.value + params.api.setEditCellValue( + { id: params.id, field: params.field, value: { ...params.row.paramAttr, example: newValue } }, + e + ) + setDirty(true) + onValueChange?.() + }} + sx={{ + '&.MuiTextField-root input': { + paddingLeft: theme.spacing(1), + paddingRight: theme.spacing(1) + } + }} + placeholder="Value" + /> + ) + } + }, + { + field: 'actions', + type: 'actions', + resizable: false, + sortable: false, + width: getActionColWidth(1), + hideable: true, + align: 'left', + getActions + } + ] + + const getEditMeta = (raw: boolean = false) => { + const editMeta = tableApiRef.current.state.editRows + traverse( + rows, + (node: BodyParamsType) => { + const editRowMeta = editMeta[node.id] + if (editRowMeta) { + Object.keys(editRowMeta).forEach((key) => { + const value = editRowMeta[key]?.value + if (!isNil(value)) { + node[key as keyof BodyParamsType] = value + } + }) + } + }, + 'childList' + ) + return rows.filter( + (row) => tableApiRef.current.state.rowSelection.includes(row.id) && (raw ? true : row.name) + ) as BodyParamsType[] + } + + const updateContentType = (contentType: ContentType | null) => { + const editRows = getEditMeta() + const targetRow = editRows.find((row) => row.name === 'Content-Type') + const id = targetRow?.id + if (contentType) { + id && + tableApiRef.current.setEditCellValue({ + id, + field: 'paramAttr', + value: { ...targetRow.paramAttr, example: contentType } + }) + } else { + setRows(rows.filter((row) => row.id !== id)) + + } + } + + const importData = (changeType: ImportMessageChangeType, data: ImportMessageOption[]) => { + if (['replace-all', 'insert-end'].includes(changeType)) { + const newRows = data.map((item) => { + return { ...EmptyRow(item.key), paramAttr: { example: item.value } } + }) + changeType === 'replace-all' && setRows(newRows as BodyParamsType[]) + changeType === 'insert-end' && setRows([...rows, ...newRows] as BodyParamsType[]) + return + } + if (['replace-changed'].includes(changeType)) { + const editMeta = getEditMeta(true) + data.forEach(({ key, value }) => { + const target = editMeta.find((row) => row.name === key) + if (target) { + target.paramAttr.example = value + } else { + editMeta.push({ ...EmptyRow(key), paramAttr: { example: value } } as BodyParamsType) + } + }) + setRows(editMeta) + } + } + + const handleSelectionChange = ( rowSelectionModel: GridRowSelectionModel)=>{ + setRows((prevRow)=>(prevRow.map((x)=>({...x,_checked:rowSelectionModel.indexOf(x.id)!== -1})))) + setRowSelectionModel(rowSelectionModel) + } + + const updateRows = (rows: BodyParamsType[]) => { + const newRows = rows?.length + ? rows.map((row) => { + return { ...EmptyRow(), ...row } + }) + : [EmptyRow()] + setRows(newRows) + setRowSelectionModel(newRows.filter(x=>x._checked)?.map((row) => row.id)) + } + + useImperativeHandle(apiRef, () => ({ + getEditMeta, + updateContentType, + importData, + updateRows + })) + + return ( + + + col) as GridColDef[]} + defaultGroupingExpansionDepth={-1} + pagination={false} + hideFooter + rowSelectionModel={rowSelectionModel} + onRowSelectionModelChange={handleSelectionChange} + autosizeOptions={{ + expand: true, + includeHeaders: false + }} + loading={loading} + slots={{ + loadingOverlay: LinearProgress + }} + disableColumnMenu={true} + disableColumnReorder={true} + disableColumnPinning={true} + disableColumnSorting={true} + /> + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/Clipboard/index.tsx b/frontend/packages/common/src/components/postcat/api/Clipboard/index.tsx new file mode 100644 index 00000000..f08b7ab5 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/Clipboard/index.tsx @@ -0,0 +1,37 @@ + +import { useState, useEffect } from 'react' +import type { ReactNode } from 'react' +import { Box } from '@mui/material' +import { IconButton } from '../IconButton' +import useCopyToClipboard from "@common/hooks/copy.ts"; + +export interface ClipboardProps { + text: string + children?: ReactNode + onSuccess?: () => void + onError?: () => void +} + +export function Clipboard(props: ClipboardProps): JSX.Element { + const { text, children, onError, onSuccess } = props + const DefaultText = '复制' + const [buttonTitle, setButtonTitle] = useState(DefaultText) + const { copyToClipboard } = useCopyToClipboard(); + const handleCopy = (): void => { + copyToClipboard(text) + } + + useEffect(() => { + const timer = setTimeout(() => { + if (buttonTitle !== DefaultText) setButtonTitle(DefaultText) + }, 2000) + + return () => clearTimeout(timer) + }, [buttonTitle]) + + return ( + + {children || } + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/Codebox/index.tsx b/frontend/packages/common/src/components/postcat/api/Codebox/index.tsx new file mode 100644 index 00000000..97d658d1 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/Codebox/index.tsx @@ -0,0 +1,193 @@ +import { memo, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react' +import type { RefObject } from 'react' +import { Box, useTheme } from '@mui/material' +import { Editor, useMonaco } from '@monaco-editor/react' +import { type editor as MonacoEditor } from 'monaco-editor' +import { IconButton } from '../IconButton' +import { message } from 'antd' + +export interface CodeboxApiRef { + insertCode: (value: string) => void + formatCode: () => void +} + +interface CodeboxProps { + options?: MonacoEditor.IStandaloneEditorConstructionOptions + value?: string + onChange?: (value: string) => void + enableToolbar?: boolean + width?: string + height?: string | null + readOnly?: boolean + apiRef?: RefObject + language?: 'html' | 'json' | 'xml' | 'javascript' | 'css' | 'plaintext' + extraContent?:React.ReactNode +} + +export const Codebox = memo((props: CodeboxProps) => { + const { + options, + value: controlledValue, + onChange, + enableToolbar = true, + width = '800px', + height, + apiRef, + readOnly = false, + language = 'plaintext', + extraContent + } = props + + const [code, setCode] = useState(``) + const editorRef = useRef(null) + const monaco = useMonaco() + + const defaultOptions: MonacoEditor.IStandaloneEditorConstructionOptions = { + scrollBeyondLastLine: false, + wordWrap: 'on', + wrappingStrategy: 'advanced', + minimap: { + enabled: false + }, + formatOnPaste: true, + formatOnType: true, + scrollbar: { + scrollByPage: true, + alwaysConsumeMouseWheel: false + }, + overviewRulerLanes: 0, + quickSuggestions: { other: true, strings: true }, + readOnly, + insertSpaces: true, + tabSize: 2 + } + + const isControlled = 'value' in props + + + const [editorHeight, setEditorHeight] = useState('5em') + const updateEditorHeight = useCallback((): void => { + const model = editorRef.current?.getModel() + if (model) { + const DefaultLineHeight = 18 + const renderHeight = Math.max(editorRef.current?.getContentHeight() || DefaultLineHeight * 5) + setEditorHeight(`${renderHeight}px`) + } + }, []) + + useImperativeHandle(apiRef, () => ({ + insertCode, + formatCode + })) + + useEffect(() => { + updateEditorHeight() + }, [updateEditorHeight]) + + const handleEditorChange = (value?: string): void => { + if (!isControlled) { + setCode(value || '') + } + onChange?.(value || '') + updateEditorHeight() + } + + const insertCode = (value: string): void => { + if (editorRef.current && monaco) { + const selection = editorRef.current.getSelection() + if (!selection) return + const range = new monaco.Range( + selection.startLineNumber, + selection.startColumn, + selection.endLineNumber, + selection.endColumn + ) + editorRef.current.executeEdits('', [ + { + range, + text: value, + forceMoveMarkers: true + } + ]) + editorRef.current.focus() + } + } + + const editorDidMount = (editor: MonacoEditor.IStandaloneCodeEditor): void => { + editorRef.current = editor + } + + const formatCode = async (): Promise => { + if (editorRef.current) { + editorRef.current.getAction('editor.action.formatDocument')?.run(); + } + } + + const copyCode = async (): Promise => { + if (editorRef.current) { + await navigator.clipboard.writeText(editorRef.current.getValue()) + message.success('复制成功') + } + } + + const searchInCode = async (): Promise => { + if (editorRef.current) { + await editorRef.current.getAction('actions.find')?.run() + } + } + + const replaceInCode = async (): Promise => { + if (editorRef.current) { + await editorRef.current.getAction('editor.action.startFindReplaceAction')?.run() + } + } + + const theme = useTheme() + + return ( + + {enableToolbar ? (<> + + {extraContent} + + + 格式化 + + + 复制 + + + 搜索 + + {!readOnly && + 替代 + } + + ) : null} + + + ) +}) diff --git a/frontend/packages/common/src/components/postcat/api/Collapse/index.tsx b/frontend/packages/common/src/components/postcat/api/Collapse/index.tsx new file mode 100644 index 00000000..29c07397 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/Collapse/index.tsx @@ -0,0 +1,111 @@ + +import { AccordionDetails, Chip, Stack, Typography, useTheme } from '@mui/material' +import MuiAccordion, { AccordionProps } from '@mui/material/Accordion'; +import MuiAccordionSummary, { + AccordionSummaryProps, +} from '@mui/material/AccordionSummary'; +import { useState, type ReactNode } from 'react' +import { Icon } from '../Icon' +import { styled } from '@mui/material/styles'; + +export interface CollapseProps { + children: ReactNode + title: string + tag?: string + key?:string +} + +export function Collapse({ children, title, tag,key }: CollapseProps): JSX.Element { + const [expanded, setExpanded] = useState(true) + + const theme = useTheme() + + const Accordion = styled((props: AccordionProps) => ( + + ))(({ theme }) => ({ + border: `1px solid #EDEDED`, + '&:not(:last-child)': { + borderBottom: 0, + }, + '&::before': { + display: 'none', + }, + })); + + +const AccordionSummary = styled((props: AccordionSummaryProps) => ( + } + {...props} + /> + ))(({ theme }) => ({ + backgroundColor: + theme.palette.mode === 'dark' + ? 'rgba(255, 255, 255, .05)' + : 'rgba(0, 0, 0, .03)', + flexDirection: 'row-reverse', + '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': { + transform: 'rotate(180deg)', + }, + '& .MuiAccordionSummary-content': { + marginLeft: theme.spacing(1), + ' .MuiTypography-root':{ + fontWeight:'bold' + } + }, + })); + + return ( + + } + aria-controls={`${title}-panel-content`} + id={`${title}-panel-header`} + onClick={(): void => setExpanded(!expanded)} + > + + {title} + {tag ? : null} + + + {children} + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/Dialog/autocomplete-dialog.tsx b/frontend/packages/common/src/components/postcat/api/Dialog/autocomplete-dialog.tsx new file mode 100644 index 00000000..cb669118 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/Dialog/autocomplete-dialog.tsx @@ -0,0 +1,161 @@ +import type { SubmitHandler } from 'react-hook-form' +import { useForm, Controller } from 'react-hook-form' +import { + Autocomplete, + Box, + Checkbox, + CircularProgress, + DialogContent, + FormControl, + FormHelperText, + TextField, + Typography, + useTheme +} from '@mui/material' +import type { SyntheticEvent } from 'react' +import { useCallback, useEffect } from 'react' +import type { BaseDialogProps } from './base-dialog' +import { BaseDialog, DialogActions } from './base-dialog' +import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank' +import CheckBoxIcon from '@mui/icons-material/CheckBox' + +export interface AutoCompleteOption { + label: string + value: string + secondary?: string + disabled?: boolean +} + +export interface AutoCompleteDialogProps extends BaseDialogProps { + title?: string + defaultValue?: AutoCompleteOption[] + placeholder?: string + options: AutoCompleteOption[] + validation?: { + required?: string | boolean + } + onInputChange?: (val: string) => void + loading?: boolean +} + +const icon = +const checkedIcon = + +export function AutoCompleteDialog(props: AutoCompleteDialogProps): JSX.Element { + const { + open, + onClose, + onConfirm, + defaultValue = '', + placeholder = '', + options, + validation = {}, + title, + onInputChange, + loading = false + } = props + const { control, handleSubmit, formState, reset } = useForm({ + defaultValues: { + value: defaultValue + } + }) + const { errors, isValid, isDirty } = formState + + const theme = useTheme() + + const resetForm = useCallback(() => { + reset({}) + }, [reset]) + + useEffect(() => { + resetForm() + }, [resetForm, open]) + + const onSubmit: SubmitHandler<{ value: AutoCompleteOption[] | string }> = (data) => { + onConfirm?.(data.value) + } + + return ( + +
+ + + { + return ( + + {...field} + loading={loading} + disableCloseOnSelect + limitTags={1} + multiple + filterOptions={(x) => x} + isOptionEqualToValue={(option, v): boolean => option.value === v.value} + getOptionLabel={(option): string => option.label} + options={options} + getOptionDisabled={(option): boolean => option.disabled || false} + renderOption={(props, option, { selected }) => ( + + + + {option.label} + + {option.secondary ? ( + + ({option.secondary}) + + ) : null} + + )} + onInputChange={(_evt: SyntheticEvent, value: string): void => { + onInputChange?.(value) + }} + renderInput={(params): JSX.Element => ( + + {loading ? : null} + {params.InputProps.endAdornment} + + ) + }} + autoComplete="off" + /> + )} + onChange={(_evt, option): void => { + field.onChange(option) + }} + value={field.value as AutoCompleteOption[]} + /> + ) + }} + /> + {errors.value ? {errors.value.message} : null} + + + null} + /> + +
+ ) +} diff --git a/frontend/packages/common/src/components/postcat/api/Dialog/base-dialog.tsx b/frontend/packages/common/src/components/postcat/api/Dialog/base-dialog.tsx new file mode 100644 index 00000000..71e650bd --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/Dialog/base-dialog.tsx @@ -0,0 +1,100 @@ + +import Dialog from '@mui/material/Dialog' +import DialogTitle from '@mui/material/DialogTitle' +import type { ButtonProps } from '@mui/material' +import { Button, DialogActions as MuiDialogActions, DialogContent } from '@mui/material' +import { type ReactNode } from 'react' +import type { LoadingButtonProps } from '@mui/lab' +import { LoadingButton } from '@mui/lab' +import {renderComponent} from "@common/utils/postcat.tsx"; + +export interface BaseDialogProps extends DialogActionProps { + open: boolean + title?: string | ReactNode + children?: ReactNode + actionRender?: ReactNode | string | null + contentRender?: ReactNode | string | null + onAnimationEnd?: () => void +} + +export function BaseDialog(props: BaseDialogProps): JSX.Element { + const { open, title, children, onClose, actionRender, contentRender, onAnimationEnd } = props + + return ( + + {title ? {title} : null} + {children ? ( + children + ) : ( + + {contentRender} + + )} + {renderComponent(actionRender, )} + + ) +} + +interface DialogActionProps { + onClose?: () => void + confirmBtn?: ReactNode | string | null + confirmText?: string + confirmDisabled?: boolean + cancelBtn?: ReactNode | string | null + cancelText?: string + loading?: boolean + // eslint-disable-next-line @typescript-eslint/no-explicit-unknown + onConfirm?: (data: unknown) => void + onCancel?: () => void + cancelProps?: ButtonProps + confirmProps?: LoadingButtonProps +} + +export function DialogActions(props: DialogActionProps): JSX.Element { + + const CancelText = '取消' + const ConfirmText = '确定' + + const { + onClose, + confirmBtn, + confirmText, + confirmDisabled, + cancelBtn, + cancelText, + loading, + onConfirm, + onCancel, + cancelProps, + confirmProps + } = props + const handleClose = (): void => { + onClose?.() + } + return ( + + {cancelBtn ?? ( + + )} + {confirmBtn ?? ( + + {confirmText ?? ConfirmText} + + )} + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/Dialog/confirm-dialog.tsx b/frontend/packages/common/src/components/postcat/api/Dialog/confirm-dialog.tsx new file mode 100644 index 00000000..58e1b58c --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/Dialog/confirm-dialog.tsx @@ -0,0 +1,36 @@ +import { DialogContent } from '@mui/material' +import type { ReactNode } from 'react' +import { Icon } from '../Icon' +import type { BaseDialogProps } from './base-dialog' +import { BaseDialog, DialogActions } from './base-dialog' + +export interface ConfirmDialogProps extends BaseDialogProps { + title?: string + context: ReactNode | string + icon?: boolean + iconColor?: string +} + +export function ConfirmDialog(props: ConfirmDialogProps): JSX.Element { + const { open, onClose, icon = true, context, title, iconColor } = props + + return ( + + + {icon ? ( + + ) : null} + {context} + + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/Dialog/index.ts b/frontend/packages/common/src/components/postcat/api/Dialog/index.ts new file mode 100644 index 00000000..c313f7e8 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/Dialog/index.ts @@ -0,0 +1,4 @@ +export * from './base-dialog' +export * from './input-dialog' +export * from './confirm-dialog' +export * from './autocomplete-dialog' diff --git a/frontend/packages/common/src/components/postcat/api/Dialog/input-dialog.tsx b/frontend/packages/common/src/components/postcat/api/Dialog/input-dialog.tsx new file mode 100644 index 00000000..238129ba --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/Dialog/input-dialog.tsx @@ -0,0 +1,84 @@ +import type { SubmitHandler } from 'react-hook-form' +import { useForm, Controller } from 'react-hook-form' +import TextField from '@mui/material/TextField' +import { DialogContent, FormControl, FormHelperText } from '@mui/material' +import { useCallback, useEffect, useRef } from 'react' +import type { BaseDialogProps } from './base-dialog' +import { BaseDialog, DialogActions } from './base-dialog' + +export interface InputDialogProps extends BaseDialogProps { + title?: string + defaultValue?: string + placeholder?: string + validation?: { + required?: string | boolean + } +} + +export function InputDialog(props: InputDialogProps): JSX.Element { + const { open, onClose, onConfirm, defaultValue = '', placeholder = '', validation = {}, title } = props + const { control, handleSubmit, formState, reset } = useForm({ + defaultValues: { + value: defaultValue + } + }) + const { errors, isValid, isDirty } = formState + + const resetForm = useCallback(() => { + reset({}) + }, [reset]) + + useEffect(() => { + resetForm() + }, [resetForm, open]) + + const onSubmit: SubmitHandler<{ value: string }> = (data) => { + onConfirm?.(data as unknown as string) + } + + const inputRef = useRef(null) + + const onAnimationEnd = (): void => { + open && inputRef.current?.focus() + } + + return ( + +
+ + + { + return ( + + ) + }} + defaultValue={defaultValue || ''} + /> + {errors.value ? {errors.value.message} : null} + + + null} + /> + +
+ ) +} diff --git a/frontend/packages/common/src/components/postcat/api/Icon/index.tsx b/frontend/packages/common/src/components/postcat/api/Icon/index.tsx new file mode 100644 index 00000000..8ab2e0fe --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/Icon/index.tsx @@ -0,0 +1,623 @@ +import type { SxProps, Theme } from '@mui/material' +import { Box } from '@mui/material' +import type { SyntheticEvent } from 'react' + +export interface IconParkIconElement extends HTMLElement { + 'icon-id'?: + | '647367' + | '684408' + | '684409' + | '684411' + | '684412' + | '684413' + | '684414' + | '686740' + | '686741' + | '686742' + | '686743' + | '686744' + | '686745' + | '686746' + | '686747' + | '686748' + | '686749' + | '686750' + | '686751' + | '686752' + | '686753' + | '686754' + | '686993' + | '687741' + | '687742' + | '691262' + | '691537' + | '691538' + | '691806' + | '695738' + | '695739' + | '695740' + | '695741' + | '695742' + | '695743' + | '695746' + | '695747' + | '695748' + | '695750' + | '695751' + | '695752' + | '695754' + | '695755' + | '695756' + | '695758' + | '695759' + | '695760' + | '695761' + | '695762' + | '695763' + | '695764' + | '695801' + | '695802' + | '695803' + | '695804' + | '695805' + | '695806' + | '695807' + | '695810' + | '695811' + | '695812' + | '695817' + | '695818' + | '695819' + | '695820' + | '695821' + | '695822' + | '695828' + | '695829' + | '695830' + | '695831' + | '695833' + | '695834' + | '695835' + | '695836' + | '695837' + | '695838' + | '695839' + | '695840' + | '695841' + | '695842' + | '695844' + | '695845' + | '695846' + | '695865' + | '695867' + | '695868' + | '695869' + | '695870' + | '695876' + | '695877' + | '695878' + | '695883' + | '695884' + | '695886' + | '695887' + | '695888' + | '695889' + | '695890' + | '695891' + | '695892' + | '695893' + | '695896' + | '695899' + | '695900' + | '695901' + | '695902' + | '695903' + | '695904' + | '695905' + | '695906' + | '695907' + | '695908' + | '695909' + | '695913' + | '695914' + | '695915' + | '695916' + | '695933' + | '695934' + | '695935' + | '695936' + | '695938' + | '695940' + | '695941' + | '695942' + | '695944' + | '695945' + | '695946' + | '695947' + | '695948' + | '695950' + | '695951' + | '695953' + | '695954' + | '695955' + | '695956' + | '695957' + | '695958' + | '695959' + | '695960' + | '695961' + | '695962' + | '695963' + | '695964' + | '695966' + | '695967' + | '695968' + | '695969' + | '695971' + | '695972' + | '695973' + | '695975' + | '695978' + | '695979' + | '695980' + | '695981' + | '695982' + | '695984' + | '695985' + | '695986' + | '695987' + | '695988' + | '695990' + | '695993' + | '695995' + | '695997' + | '695999' + | '696002' + | '696003' + | '696004' + | '696005' + | '696007' + | '696009' + | '696010' + | '696011' + | '696012' + | '696013' + | '696014' + | '696015' + | '696016' + | '696017' + | '696018' + | '696019' + | '696020' + | '696021' + | '696022' + | '696023' + | '696024' + | '696025' + | '696027' + | '696028' + | '696029' + | '696030' + | '696031' + | '696032' + | '696033' + | '696034' + | '696035' + | '696036' + | '696037' + | '696038' + | '696039' + | '696040' + | '696041' + | '696042' + | '696043' + | '696044' + | '696045' + | '696046' + | '696048' + | '696049' + | '696660' + | '696661' + | '744163' + | '744173' + | '744175' + | '750656' + | '752737' + | '756392' + | '757321' + | '757499' + | '757504' + | '757518' + | '757519' + | '757520' + | '757521' + | '757616' + | '757650' + | '767277' + | '767278' + | '775549' + | '779333' + | '779418' + | '779705' + | '779706' + | '787702' + | '788577' + | '802334' + | '804269' + | '804612' + | '804614' + | '806103' + | '813707' + | '815901' + | '820089' + | '826687' + | '854318' + | '855246' + | '855247' + | '855248' + | '855927' + | '855928' + | '855929' + | '855938' + | '857931' + | '857985' + | '861388' + | '876705' + | '884011' + | '885387' + | '897026' + | '915485' + | '929257' + | '932197' + | '949128' + | '970590' + | '973801' + | '985435' + | '1002903' + | '1021623' + | '1021686' + | '1035721' + | '1035737' + | '1037074' + | '1037815' + | '1037816' + | '1037817' + | '1039918' + | '1042170' + | '1042171' + name?: + | 'round-fill' + | 'apinto-pro-icon' + | 'apinto-icon' + | 'apinto-pro' + | 'apinto' + | 'check-circle' + | 'apispace' + | 'auto-generate-api' + | 'compare-api' + | 'multi-protocal' + | 'read-good' + | 'richdoc' + | 'mockapi' + | 'script-support' + | 'diy-test' + | 'send' + | 'stereo-perspective' + | 'automatic-robot' + | 'switch-env' + | 'flash' + | 'chart-pie' + | 'date-drive' + | 'apistudio' + | 'postcat-icon' + | 'postcat' + | 'apistudio-icon' + | 'update-rotation' + | 'page' + | 'apispace-icon' + | 'avatar' + | 'people' + | 'people-minus' + | 'people-plus' + | 'peoples' + | 'user-business' + | 'folder-close-fill' + | 'windows' + | 'github' + | 'qq' + | 'browser-chrome' + | 'linux' + | 'edge' + | 'wechat' + | 'browser' + | 'gitlab' + | 'apple' + | 'alipay' + | 'facebook' + | 'twitter' + | 'paypal' + | 'new-lark' + | 'delete' + | 'return' + | 'search' + | 'import' + | 'export' + | 'add' + | 'add-child' + | 'file-addition' + | 'add-circle' + | 'minus' + | 'close' + | 'close-small' + | 'check-small' + | 'check' + | 'code-terminal' + | 'code' + | 'preview-open' + | 'preview-close' + | 'folder-close' + | 'folder-open' + | 'upload' + | 'download' + | 'copy' + | 'upload-file' + | 'compare' + | 'edit' + | 'share' + | 'share-all' + | 'share-url-fill' + | 'share-url' + | 'back' + | 'back-fill' + | 'share-fill' + | 'sort' + | 'filter' + | 'reduce' + | 'done-all' + | 'full-selection' + | 'right-bar' + | 'left-bar' + | 'direction-adjustment' + | 'down-small' + | 'left-small' + | 'right-small' + | 'right-one' + | 'right' + | 'up' + | 'up-one' + | 'up-small' + | 'up-two' + | 'down-two' + | 'enter' + | 'down' + | 'left' + | 'down-one' + | 'left-two' + | 'right-two' + | 'left-one' + | 'more' + | 'expand-left' + | 'expand-right' + | 'column' + | 'center-alignment' + | 'list-add' + | 'sort-amount-down' + | 'sort-amount-up' + | 'list' + | 'remind' + | 'close-remind' + | 'api' + | 'rocket' + | 'monitor' + | 'robot' + | 'plan' + | 'application' + | 'chart-proportion' + | 'data' + | 'chart-line' + | 'pie-10' + | 'pie' + | 'chart-bubble' + | 'cube' + | 'application-menu' + | 'crown' + | 'crown-fill' + | 'market' + | 'file-word' + | 'file-excel' + | 'hashtag-key' + | 'file-hash' + | 'refresh' + | 'order' + | 'command' + | 'branch' + | 'page-template' + | 'smart-optimization' + | 'assembly-line' + | 'stopwatch' + | 'checklist' + | 'menu-fold' + | 'menu-unfold' + | 'alarm' + | 'protection' + | 'caution' + | 'openapi' + | 'webhook' + | 'holding-hands' + | 'support' + | 'agreement' + | 'community' + | 'roadmap' + | 'family-7knl2ae1' + | 'smiling-face' + | 'play-fill' + | 'play' + | 'pause' + | 'magic' + | 'whole-site-accelerator' + | 'link-cloud-faild' + | 'link-cloud-sucess' + | 'translate' + | 'funds' + | 'unhappy-face' + | 'message' + | 'connection-arrow' + | 'loading' + | 'fork' + | 'quote' + | 'headset' + | 'attention' + | 'theme' + | 'keyboard' + | 'briefcase' + | 'star' + | 'star-7knmka28' + | 'protect' + | 'finance' + | 'setting' + | 'link' + | 'undo' + | 'inbox-success' + | 'home' + | 'local' + | 'laptop' + | 'view-list' + | 'lock' + | 'unlock' + | 'lightning' + | 'file-text' + | 'cooperative-handshake' + | 'navigation' + | 'view-grid-detail' + | 'help' + | 'history' + | 'logout-7knnioon' + | 'chinese' + | 'calendar' + | 'play-cycle' + | 'world' + | 'plugins' + | 'link-cloud' + | 'book' + | 'table-report' + | 'qiyeweixin' + | 'Oauth' + | 'dingding' + | 'eolink' + | 'tool' + | 'category-management' + | 'folder-code-one' + | 'link-three-8ah7lifn' + | 'download-two-8ah85008' + | 'quanjusuoxiao1' + | 'quanjufangda21' + | 'quanjusuoxiao211' + | 'quanjufangda1' + | 'wenjianshezhi' + | 'key' + | 'zidingyijiaoben' + | 'tiqubianliang' + | 'mock' + | 'tongzhishezhi' + | 'csdn' + | 'ceshibaogao' + | 'biangengtongzhi' + | 'icon-api' + | 'youjian' + | 'pushpin' + | 'announcement' + | 'collapse-text-input' + | 'zhankai' + | 'replay-music' + | 'download-web' + | 'permissions' + | 'file-editing' + | 'wallet' + | 'file-focus' + | 'pingpu-9a913n0n' + | 'zuoyoufenping-9a913n1f' + | 'shangxiafenping-9a913n1i' + | 'Paypal11' + | 'zhifubaozhifu1' + | 'weixinzhifu11' + | 'weixinzhifu' + | 'update-rotation-9and40f5' + | 'terminal' + | 'switch' + | 'zhinengrucan' + | 'biaoqian-banbenleixinzeng' + | 'book-open' + | 'morentouxiang-2' + | 'xiajia' + | 'drag' + | 'new-up' + | 'rss' + | 'yewuchangjing' + | 'newlybuild' + | 'bianji' + | 'jiekoushouquan' + | 'interfacefenzutubiao' + | 'yidong' + | 'link-one' + | 'canshugouzaoqi' + | 'bianliang' + | 'tars' + | 'if' + | 'tars-2' + | 'yingyongguanxi' + | 'save-one' + | 'save' + size?: string + width?: string + height?: string + color?: string + stroke?: string + fill?: string + rtl?: string + spin?: string +} + + +interface IconProps extends IconParkIconElement { + px: number + /** Padding left */ + pl: number + /** Padding right */ + pr: number + /* ml + mr */ + mx: number + /** Margin left */ + ml: number + /** Margin right */ + mr: number + sx: SxProps + onClick?: (event: SyntheticEvent) => void +} + +declare namespace JSX { + interface IntrinsicElements { + 'iconpark-icon': Partial + } +} + +// @ts-ignore +export function Icon({name, size, fill, color, px, mx, ml, mr, pl, pr, sx, onClick}: Partial): JSX.Element { + // @ts-ignore + return ( + + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/IconButton/index.tsx b/frontend/packages/common/src/components/postcat/api/IconButton/index.tsx new file mode 100644 index 00000000..0d791131 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/IconButton/index.tsx @@ -0,0 +1,66 @@ +import type { ButtonProps } from '@mui/material' +import { Box, IconButton as MuiIconButton, Tooltip } from '@mui/material' +import type { ReactNode } from 'react' +import { LoadingButton } from '@mui/lab' +import { Icon, IconParkIconElement } from '../Icon' +import { RotatingWrapper } from '../RotatingWrapper' +import {useEffect} from "react"; + +interface IconButtonProps extends ButtonProps { + /** tooltip title */ + title: string + iconColor: IconParkIconElement['color'] + iconSize: IconParkIconElement['size'] + fill: IconParkIconElement['fill'] + name: IconParkIconElement['name'] + icon: boolean + loading?: boolean + children: ReactNode +} + +export function IconButton(props: Partial): JSX.Element { + const { + name, + title, + size, + fill, + color, + children, + disabled, + iconColor, + iconSize, + variant, + icon = false, + loading = false, + ...otherProps + } = props + const isIcon = children || icon + const padding = isIcon ? '6px 8px' : null + + return ( + + {isIcon ? ( + + + {children} + + ) : ( + + + + + + + + )} + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/MoreSetting/components/Example.tsx b/frontend/packages/common/src/components/postcat/api/MoreSetting/components/Example.tsx new file mode 100644 index 00000000..e029950f --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/MoreSetting/components/Example.tsx @@ -0,0 +1,45 @@ +import { Box, Typography, useTheme } from '@mui/material' +import {Codebox} from "../../Codebox"; + +interface ExampleProps { + code: string + onChange?: (code: string) => void + readOnly?: boolean +} + +export function Example({ code, onChange, readOnly = false }: ExampleProps) { + + const theme = useTheme() + return ( + + + + 示例 + + + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/MoreSetting/components/ParamLimit.tsx b/frontend/packages/common/src/components/postcat/api/MoreSetting/components/ParamLimit.tsx new file mode 100644 index 00000000..f6fb5123 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/MoreSetting/components/ParamLimit.tsx @@ -0,0 +1,113 @@ + +import { ChangeEvent, useEffect, useState } from 'react' +import { FormControl, TextField, Box } from '@mui/material' + +interface ParamLimitProps { + min: number | null + max: number | null + onChange: ({ min, max }: { min: number; max: number }) => void + minLabel?: string + maxLabel?: string +} + +export function ParamLimit({ min, max, onChange, minLabel = 'Minimum', maxLabel = 'Maximum' }: ParamLimitProps) { + const [minValue, setMinValue] = useState(min ?? 0) + const [maxValue, setMaxValue] = useState(max ?? 0) + const [error, setError] = useState(null) + + const validate = (minVal: number, maxVal: number) => { + if (isNaN(minVal) || minVal < 0) { + return `The ${minLabel} must not be negative.` + } + + if (isNaN(maxVal) || maxVal < 0) { + return `The ${maxLabel} must not be negative.` + } + + if (minVal > maxVal) { + return `The ${maxLabel} must be greater than or equal to the ${minLabel}.` + } + + return null + } + + useEffect(() => { + onChange?.({ + min: minValue, + max: maxValue + }) + }, [minValue, maxValue, onChange]) + + const handleMinChange = (event: ChangeEvent) => { + const newMinValue = parseFloat(event.target.value) + setMinValue(newMinValue) + setError(validate(newMinValue, maxValue)) + } + + const handleMaxChange = (event: ChangeEvent) => { + const newMaxValue = parseFloat(event.target.value) + setMaxValue(newMaxValue) + setError(validate(minValue, newMaxValue)) + } + + return ( + +
+ + + + + + + + + + + +
+ ) +} diff --git a/frontend/packages/common/src/components/postcat/api/MoreSetting/components/ParamPreview.tsx b/frontend/packages/common/src/components/postcat/api/MoreSetting/components/ParamPreview.tsx new file mode 100644 index 00000000..3b30c625 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/MoreSetting/components/ParamPreview.tsx @@ -0,0 +1,98 @@ +import { Box } from '@mui/material' +import { DataGridPro, GridColDef, useGridApiRef } from '@mui/x-data-grid-pro' +import { useEffect, useMemo } from 'react' +import {previewTableHoverSx } from '../../PreviewTable' + +interface ParamPreviewProps { + name?: string + type?: string + required?: boolean + description?: string +} + +export function ParamPreview(props: ParamPreviewProps) { + const { name, type, required, description } = props + const apiRef = useGridApiRef() + + const rows = useMemo(() => { + return [ + { + id: '0', + name, + type, + required, + description + } + ] + }, [name, type, required, description]) + + + useEffect(() => { + // setTimeout(()=>{ + // const element = document.querySelectorAll('.MuiDataGrid-main'); + // if(element?.length > 0){ + // for(const x of element){ + // x.childNodes[x.childNodes.length - 1 ].textContent === 'MUI X Missing license key' ? x.childNodes[x.childNodes.length - 1 ].textContent = '' :null + // } + // } + // },500) + }, []); + + const hoverSx = useMemo(() => { + return { + ...previewTableHoverSx() + } + }, []) + + + const columns: GridColDef[] = [ + { + field: 'name', + headerName: '参数名', + width: 120 + }, + { + field: 'type', + headerName: '类型', + width: 120 + }, + { + field: 'required', + headerName: '必需', + sortable: false, + valueGetter: (params) => Boolean(params.row.isRequired), + type: 'boolean', + width: 200 + }, + { + field: 'description', + headerName: '描述', + flex: 1 + } + ] + + return ( + + row.path} + hideFooter + autosizeOptions={{ + expand: true, + includeHeaders: false + }} + columnHeaderHeight={40} + rowHeight={40} + disableColumnMenu={true} + disableColumnReorder={true} + disableColumnPinning={true} + disableColumnSorting={true} + /> + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/MoreSetting/components/ValueEnum.tsx b/frontend/packages/common/src/components/postcat/api/MoreSetting/components/ValueEnum.tsx new file mode 100644 index 00000000..98d540b3 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/MoreSetting/components/ValueEnum.tsx @@ -0,0 +1,208 @@ + +import { TextField, useTheme } from '@mui/material' +import { + DataGridPro, + GridColDef, + GridRenderEditCellParams, + GridRowModesModel, + GridRowParams, + useGridApiRef +} from '@mui/x-data-grid-pro' +import { RefObject, useCallback, useEffect, useImperativeHandle, useState } from 'react' +import {IconButton} from "../../IconButton"; +import {flattenTree, generateId, getActionColWidth} from "@common/utils/postcat.tsx"; +import {EditableDataGridSx} from "../../ApiManager/components/EditableDataGrid"; +import { commonTableSx } from '@common/const/api-detail/index.ts'; + +export interface ValueEnum { + value: string + description: string +} + +export interface ValueEnumApi { + getEditMeta: () => Partial[] +} + +interface ValueEnumProps { + data: ValueEnum[] | null + apiRef?: RefObject + readOnly?:boolean +} + +interface Row extends ValueEnum { + id: string +} + +class EmptyRow implements Row { + constructor(val?: string, description?: string) { + this.id = generateId() + this.value = val || '' + this.description = description || '' + } + id = '' + value = '' + description = '' +} + +export function ValueEnum({ data, apiRef,readOnly = false }: ValueEnumProps) { + const [rowModesModel, setRowModesModel] = useState({}) + const tableApiRef = useGridApiRef() + const [rows, setRows] = useState([new EmptyRow()]) + const [renderRows, setRenderRows] = useState([]) + const theme = useTheme() + + + useEffect(() => { + if (data?.length) { + const newRows = data.map((row) => ({ ...row, id: generateId() })) + setRows(newRows) + } + }, [data]) + + useEffect(() => { + const newRenderRows = flattenTree(rows) + setRenderRows(newRenderRows) + const rowModesModel = newRenderRows.reduce((acc, cur) => ({ ...acc, [cur.id]: { mode: 'edit' } }), {}) + setRowModesModel(rowModesModel) + }, [rows]) + + const getEditMeta = () => { + const editRows: Partial[] = rows.map((row) => { + const editMeta = tableApiRef.current.state.editRows[row.id] + const editMetaCollections = Object.keys(editMeta).reduce( + (acc, cur) => { + return Object.assign(acc, { + [cur]: editMeta[cur].value + }) + }, + { id: row.id } + ) + return editMetaCollections + }) + return editRows + } + + useImperativeHandle(apiRef, () => ({ + getEditMeta + })) + + const handleRowDelete = useCallback( + (params: GridRowParams) => { + setRows(rows.filter((row) => row.id !== params.row.id)) + }, + [rows] + ) + + + const columns: GridColDef[] = [ + { + field: 'value', + headerName: '值枚举', + type: 'string', + sortable: false, + flex: 1, + editable: !readOnly, + align: 'left', + headerAlign: 'left', + renderEditCell(params: GridRenderEditCellParams) { + return ( + { + const newValue = e.target.value as string + const rowIndex = params.row.__globalIndex__ + params.api.setEditCellValue({ id: params.row.id, field: 'value', value: newValue }) + const rowIds = params.api.getAllRowIds() + if (rowIds.length === rowIndex + 1) { + const newRow = new EmptyRow() + setRows([...rows, newRow]) + } + }} + /> + ) + } + }, + { + field: 'description', + headerName: '描述', + type: 'string', + sortable: false, + flex: 1, + editable: !readOnly, + align: 'left', + headerAlign: 'left', + renderEditCell(params) { + return ( + { + const newValue = e.target.value + params.api.setEditCellValue({ id: params.id, field: params.field, value: newValue }, e) + }} + sx={{ + input: { + paddingLeft: `${theme.spacing(1)} !important`, + paddingRight: `${theme.spacing(1)} !important` + } + }} + placeholder='示例' + /> + ) + } + }, + { + field: 'actions', + type: 'actions', + resizable: false, + sortable: false, + width: getActionColWidth(1), + hideable: true, + getActions: (params) => { + if (renderRows.length <= 1) return [] + return [ + { + handleRowDelete(params) + }} + /> + ] + } + } + ] + + return ( + <> + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/MoreSetting/index.tsx b/frontend/packages/common/src/components/postcat/api/MoreSetting/index.tsx new file mode 100644 index 00000000..4b7ac44c --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/MoreSetting/index.tsx @@ -0,0 +1,115 @@ +import { Box, Stack } from '@mui/material' +import { ParamPreview } from './components/ParamPreview' +import { RenderMessageBody } from '../ApiPreview/components/MessageBody' + +import { useEffect, useRef, useState } from 'react' +import { ValueEnum, ValueEnumApi } from './components/ValueEnum' +import { Example } from './components/Example' +import { ParamLimit } from './components/ParamLimit' +import {ParamAttrType} from "@common/const/api-detail"; +import { BaseDialog} from "../Dialog/base-dialog.tsx"; +import {ApiParamsTypeOptions} from "../ApiManager/components/ApiMessageBody/constants.ts"; + +interface MoreSettingProps { + open: boolean + onClose: () => void + param: RenderMessageBody | null + onConfirm?: () => void + onChange?: ({ param, id }: { param: Partial; id: string }) => void + readOnly?: boolean + hiddenConfig?: { + valueEnum?: boolean + value?: boolean + example?: boolean + paramLength?: boolean + } +} + +export function MoreSetting({ open, readOnly,onClose, param, onChange, hiddenConfig }: MoreSettingProps) { + const [previewType, setPreviewType] = useState('') + const [valueEnumList, setValueEnumList] = useState([]) + + const valueEnumApiRef = useRef(null) + + const [code, setCode] = useState('') + const [minLength, setMinLength] = useState(0) + const [maxLength, setMaxLength] = useState(0) + const [minValue, setMinValue] = useState(0) + const [maxValue, setMaxValue] = useState(0) + + useEffect(() => { + setPreviewType(ApiParamsTypeOptions.find((option) => option.value === param?.dataType)?.key || '') + try { + const list = JSON.parse(param?.paramAttr?.paramValueList || '[]') + setValueEnumList(list.length ? list : []) + } catch (err) { + console.warn('error parsing paramValueList', err) + setValueEnumList([]) + } + }, [param]) + + const handleConfirm = () => { + onChange?.({ + id: param?.id as string, + param: { + paramValueList: JSON.stringify(valueEnumApiRef.current?.getEditMeta()), + minLength, + maxLength, + minValue, + maxValue, + example: code + } + }) + } + + const handleExampleChange = (code: string) => { + setCode(code) + } + + const handleParamLengthChange = ({ min, max }: { min: number; max: number }) => { + setMinLength(min) + setMaxLength(max) + } + + const handleParamValueChange = ({ min, max }: { min: number; max: number }) => { + setMinValue(min) + setMaxValue(max) + } + + return ( + + + + + {!hiddenConfig?.paramLength ? ( + + ) : null} + {!hiddenConfig?.value ? ( + + ) : null} + {!hiddenConfig?.valueEnum ? : null} + {!hiddenConfig?.example ? ( + + ) : null} + + + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/PreviewTable/index.tsx b/frontend/packages/common/src/components/postcat/api/PreviewTable/index.tsx new file mode 100644 index 00000000..880e8ca4 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/PreviewTable/index.tsx @@ -0,0 +1,41 @@ + +import { GridActionsCellItem } from '@mui/x-data-grid-pro' +import type { ReactNode } from 'react' +import type { SxProps, Theme } from '@mui/material' +import { IconButton } from '../IconButton' +import { commonTableSx } from '@common/const/api-detail' + +interface PreviewGridActionsCellItemProps { + icon: IconParkIconElement['name'] + label: string + onClick?: () => void +} + +export function PreviewGridActionsCellItem({ icon, label, onClick }: PreviewGridActionsCellItemProps): ReactNode { + return ( + } + label={label} + /> + ) +} + +export function previewTableHoverSx(): SxProps { + return {...commonTableSx + } +} + +export function collapseTableSx(borderRadius: string | number): SxProps { + return { + border: 'none', + borderRadius: `0 0 ${borderRadius} ${borderRadius}`, + overflow: 'hidden' + } +} diff --git a/frontend/packages/common/src/components/postcat/api/RequestMethod/index.tsx b/frontend/packages/common/src/components/postcat/api/RequestMethod/index.tsx new file mode 100644 index 00000000..ad08a742 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/RequestMethod/index.tsx @@ -0,0 +1,100 @@ + +import { Chip, Skeleton } from '@mui/material' +import { useEffect, useState } from 'react' + +export enum Protocol { + HTTP, + HTTPS, + WS, + WSS, + TCP, + UDP, + SOCKET, + WEBSOCKET, + SOAP, + HSF, + DUBBO, + GRPC +} + +export enum HTTPMethod { + POST, + GET, + PUT, + DELETE, + HEAD, + OPTIONS, + PATCH +} + +interface MethodColor { + color: string + bgColor: string +} + +const methodColorMapping: { [key in HTTPMethod]: MethodColor } = { + [HTTPMethod.GET]: { + color: 'rgba(6, 125, 219, 1)', + bgColor: 'rgba(6, 125, 219, .15)' + }, + [HTTPMethod.POST]: { + color: 'rgba(16, 165, 75, 1)', + bgColor: 'rgba(16, 165, 75, .15)' + }, + [HTTPMethod.PUT]: { + color: 'rgba(216, 131, 12, 1)', + bgColor: 'rgba(216, 131, 12, .15)' + }, + [HTTPMethod.DELETE]: { + color: 'rgba(194, 22, 27, 1)', + bgColor: 'rgba(194, 22, 27, .15)' + }, + [HTTPMethod.HEAD]: { + color: 'rgba(238, 196, 12, 1)', + bgColor: 'rgba(238, 196, 12, 0.15)' + }, + [HTTPMethod.OPTIONS]: { + color: 'rgba(14, 90, 179, 1)', + bgColor: 'rgba(14, 90, 179, 0.15)' + }, + [HTTPMethod.PATCH]: { + color: 'rgba(119, 40, 245, 1)', + bgColor: 'rgba(119, 40, 245, 0.15)' + } +} + +export interface RequestMethodProps { + protocol: Protocol + method: string + variant?: 'default' | 'filled' + displayFormat?: 'abbreviation' | 'full' + loading?: boolean +} + +export function RequestMethod({ + method, + // protocol, + variant = 'default', + displayFormat = 'abbreviation', + loading = false +}: RequestMethodProps): JSX.Element { + const [label, setLabel] = useState('Unknown') + + useEffect(() => { + const methodName = method + const isOverLong = methodName?.length > 5 + const displayLabel = displayFormat === 'abbreviation' && isOverLong ? methodName.slice(0, 3) : methodName + setLabel(displayLabel) + }, [displayFormat, method]) + + const transparent = 'transparent' + + const chipStyle = { + height:'22px', + borderRadius: '4px', + color: methodColorMapping[method]?.color || '#333', + backgroundColor: variant === 'default' ? transparent : methodColorMapping[method]?.bgColor + } + + return !loading ? : +} diff --git a/frontend/packages/common/src/components/postcat/api/RotatingWrapper/index.tsx b/frontend/packages/common/src/components/postcat/api/RotatingWrapper/index.tsx new file mode 100644 index 00000000..98494096 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/RotatingWrapper/index.tsx @@ -0,0 +1,42 @@ +import { styled } from '@mui/material' +import type { ReactNode } from 'react' + +export interface RotatingWrapperProps { + rotation?: number + duration?: number + iterationCount?: string + children?: ReactNode +} + +const RotatingComponent = styled('div', { + shouldForwardProp: (prop) => prop !== 'rotation' && prop !== 'duration' +})(({ rotation = 0, duration = 0 }) => { + const isStop = rotation === 0 || duration === 0 + return { + display: 'inline-block', + animation: isStop ? 'none' : `rotate ${duration}s linear infinite`, + '@keyframes rotate': { + '0%': { + transform: 'rotate(0deg)' + }, + '100%': { + transform: `rotate(${rotation}deg)` + } + } + } +}) + +export function RotatingWrapper({ + children, + rotation = 360, + duration = 1, + iterationCount = 'infinite' +}: RotatingWrapperProps): JSX.Element { + return ( + + {children} + + ) +} + +export default RotatingWrapper diff --git a/frontend/packages/common/src/components/postcat/api/Upload/index.tsx b/frontend/packages/common/src/components/postcat/api/Upload/index.tsx new file mode 100644 index 00000000..ffff6448 --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/Upload/index.tsx @@ -0,0 +1,67 @@ +import { Box, Chip, Paper, Typography, useTheme } from '@mui/material' +import { useCallback } from 'react' +import { useDropzone } from 'react-dropzone' +import { useAutoAnimate } from '@formkit/auto-animate/react' +import { Icon } from '../Icon' +import { IconButton } from '../IconButton' + +export interface UploadProps { + value?: File | null + onChange?: (value: File | null) => void +} + +export function Upload({ value, onChange }: UploadProps): JSX.Element { + const onDrop = useCallback( + (acceptedFiles: File[]) => { + if (acceptedFiles.length) { + onChange?.(acceptedFiles[0]) + } + }, + [onChange] + ) + + const [parent] = useAutoAnimate() + + const { getRootProps, getInputProps } = useDropzone({ onDrop }) + + const theme = useTheme() + + + return ( + + + + + + + {'将文件拖拽至此处上传,或点击选择文件上传'} + + {value ? ( + + + {value.name} + onChange?.(null)} /> + + } + /> + + ) : null} + + ) +} diff --git a/frontend/packages/common/src/components/postcat/api/UploadButton/index.tsx b/frontend/packages/common/src/components/postcat/api/UploadButton/index.tsx new file mode 100644 index 00000000..e34b7f7a --- /dev/null +++ b/frontend/packages/common/src/components/postcat/api/UploadButton/index.tsx @@ -0,0 +1,50 @@ +import { Box, Button, Typography } from '@mui/material' +import type { ChangeEvent } from 'react' +import { useRef } from 'react' +import {file2Base64} from "@common/utils/postcat.tsx"; + +interface UploadButtonProps { + value?: + | { + name: string + content: string + }[] + | null + onChange?: ( + base64List: { + name: string + content: string + }[] + ) => void +} + +export function UploadButton({ value, onChange }: UploadButtonProps): JSX.Element { + const fileInputRef = useRef(null) + + const handleButtonClick = (): void => { + fileInputRef.current?.click() + } + + const handleFileChange = async (event: ChangeEvent): Promise => { + const files = event.target.files + const filesArray = Array.from(files || []) + const promises = filesArray.map((file) => file2Base64(file)) + const result = await Promise.all(promises) + onChange?.( + result.map((file, fileIndex) => ({ + name: filesArray[fileIndex].name, + content: file + })) + ) + } + + return ( + + + + {value?.length ? Files Selected: {value.length} : null} + + ) +} diff --git a/frontend/packages/common/src/const/api-detail/default-header.ts b/frontend/packages/common/src/const/api-detail/default-header.ts new file mode 100644 index 00000000..dc32fc23 --- /dev/null +++ b/frontend/packages/common/src/const/api-detail/default-header.ts @@ -0,0 +1,16 @@ +type DEFAULT_HEADER_OBJ_TYPE = { + [key: string]: { description: string; value: string }; + // 索引签名接受任意字符串类型的参数,并返回具有指定属性的对象 +}; +const DEFAULT_HEADER_OBJ: DEFAULT_HEADER_OBJ_TYPE = { + // 'Authorization-Type': { + // description: '鉴权方式,值为:apikey', + // value: 'apikey' + // }, + 'X-APISpace-Token': { + description: '鉴权私钥,可登陆后在管理后台的[访问控制]页面查看', + value: '' + } +} + +export default DEFAULT_HEADER_OBJ \ No newline at end of file diff --git a/frontend/packages/common/src/const/api-detail/index.ts b/frontend/packages/common/src/const/api-detail/index.ts new file mode 100644 index 00000000..74739155 --- /dev/null +++ b/frontend/packages/common/src/const/api-detail/index.ts @@ -0,0 +1,308 @@ +import {extend} from "lodash-es"; +import {HTTPMethod, Protocol} from "@common/components/postcat/api/RequestMethod"; + +export interface MenuItem { + key?: string; + name?: string; + emoji?: string; + path?: string; + content?: unknown +} + +export const GetMenuItem = (org_domain_id: string, project_domain_id: string, commonQuestionRes: unknown) => { + const Menus = [ + { key: 'introduction', name: '介绍', emoji: '📃', path: `/${org_domain_id}/api/${project_domain_id}/introduction` }, + { key: 'apiDocument', name: 'API 文档', emoji: '🔗', path: `/${org_domain_id}/api/${project_domain_id}/apiDocument` }, + { key: 'price', name: '价格套餐', emoji: '💎', path: `/${org_domain_id}/api/${project_domain_id}/price` }, + { key: 'guidence', name: '接入指南', emoji: '💡', path: `/${org_domain_id}/api/${project_domain_id}/guidence` } + ]; + + if (commonQuestionRes?.success && commonQuestionRes?.data?.content) { + Menus.splice(2, 0, { key: 'commonQuestion', name: '常见问题', emoji: '🌷', path: `/${org_domain_id}/api/${project_domain_id}/commonQuestion` }); + } + + return Menus; +}; + + +export const SKU_LIST = [ + { + name: '流量包', + key: 'flow' + }, + { + name: '订阅套餐', + key: 'subscribe' + } +] + +export const PROMISE_TEXT = ['服务保障', '未使用部分七天无理由退款', '正规企业商品来源', '交易流程全程监控'] + +export const DATA_TYPE = { + JSON: '[json]', + INT: '[int]', + FLOAT: '[float]', + DOUBLE: '[double]', + DATE: '[date]', + DATETIME: '[datetime]', + BOOLEAN: '[boolean]', + BYTE: '[byte]', + SHORT: '[short]', + LONG: '[long]', + ARRAY: '[array]', + OBJECT: '[object]', + NUMBER: '[number]', + NULL: '[null]', + FILE: '[file]', + STRING: '[string]' +} + +export type DATA_TYPE_ITEM_TYPE = keyof typeof DATA_TYPE; + +// tdk和schema使用,商品详情内页的类型 +export const PageTypeEnum = { + INTRODUCTION:1, + COMMON_QUESTION:3, + API_DOCUMENT:4, + PRICE:5, + GUIDANCE:6 +} + +export type ApiParamsBasicType = { + id: string, + parentId: number, + apiUuid: string, + responseUuid: string, + name: string, + paramType: number, + partType: number, + dataType: number, + dataTypeValue: string, + structureId: number, + structureParamId: string, + contentType: ApiBodyType , + isRequired: number, + binaryRawData: string, + description: string, + orderNo: number, + isDefault: number, + paramAttr: ParamAttrType + childList: [] + responseParams?: HttpResponseMessage +} + +export type HeaderParamsType = ApiParamsBasicType + +export type BodyParamsType = ApiParamsBasicType + +export type QueryParamsType = ApiParamsBasicType + +export type RestParamsType = ApiParamsBasicType + +export type ParamAttrType = { + id: number, + apiParamId: number, + minLength: number, + maxLength: number, + minValue: {}, + maxValue: {}, + paramLimit: string, + paramValueList: string, + paramMock: string, + attr: string, + structureIsHide: number, + example: string, + dbArr: string, + paramNote: string +} + +export type ResultListType = { + id: string, + apiUuid: string, + name: string, + httpCode: string, + httpContentType: string, + type: number, + content: string, + createUserId: number, + updateUserId: number, + createTime: number, + updateTime: number +} + +export type ResponseList = { + id: number, + responseUuid: string, + apiUuid: string, + oldId: number, + name: string, + httpCode: string, + contentType: number, + isDefault: number, + updateUserId: number, + createUserId: number, + createTime: number, + updateTime: number, + responseParams: { + headerParams: HeaderParamsType[], + bodyParams: BodyParamsType[] + queryParams: QueryParamsType[], + restParams: RestParamsType[] + } +} + +export type ApiDetail = { + id: string, + service: string, + name: string, + protocol: Protocol, + method:HTTPMethod, + uri: string, + encoding: string, + tag: string, + requestParams: { + headerParams: HeaderParamsType[], + bodyParams: BodyParamsType[], + queryParams: QueryParamsType[], + restParams: RestParamsType[] + }, + resultList: ResultListType[], + responseList: ResponseList[] +} + +export enum ApiParamsType { + string, + file, + json, + int, + float, + double, + date, + datetime, + boolean, + byte, + short, + long, + array, + object, + number, + null +} + +export type FileExample = { + name: string + content: string +}[] + +export type Example = string | FileExample + +/** Content-Type ? */ +export enum ApiBodyType { + FormData = 0, + Raw = 1, + JSON = 2, + XML = 3, + Binary = 4, + JSONArray = 6 +} + +export type TestApiBodyType = ApiBodyType.FormData | ApiBodyType.Raw | ApiBodyType.Binary + + +export type ParseCurlResult = { + /* 请求地址 */ + url: string + /* 请求方法 */ + method: string + /* 请求头部字段 */ + headers: { [key: string]: string } + /* 请求 query 参数 */ + query?: { [key: string]: string } + /* 请求内容类型 */ + contentType?: string + /* 请求 body 原文 */ + body?: string + /* 如果是 formData 会解析成对象 */ + requestParams?: { [key: string]: unknown } | string +} + +declare interface HttpResponseMessage { + bodyParams: ApiParamsBasicType[] + responseParams: ApiParamsBasicType[] + headerParams?: ApiParamsBasicType[] +} + + +export const commonTableSx = { + '.MuiDataGrid-columnHeaderTitle':{ + fontSize:'14px' + }, + '.MuiDataGrid-columnHeader':{ + background:'#f7f8fa', + + }, + '.MuiDataGrid-withBorderColor':{ + borderColor:'#EDEDED' + }, + '& .MuiButtonBase-root.MuiIconButton-root':{ + borderRadius:'4px' + }, + '& .MuiButtonBase-root.MuiIconButton-root:hover':{ + backgroundColor:'#f7f8fa' + }, + '& .MuiDataGrid-columnSeparator--resizable:hover':{ + color:'#EDEDED' + }, + + '& .MuiDataGrid-columnHeader:focus-within':{ + outline:'none'} + , + '& .MuiDataGrid-cell:focus-within':{ + outline:'none'}, + '.MuiDataGrid-columnHeaderTitleContainer':{ + justifyContent:'space-between' + }, + '.MuiDataGrid-columnSeparator--resizable:hover':{ + color:'#EDEDED' + }, + '& .MuiDataGrid-withBorderColor':{ + borderColor:'#EDEDED' + }, + '& .MuiDataGrid-row.Mui-selected ':{ + backgroundColor:'#f7f8fa', + }, + '& .MuiDataGrid-columnHeader':{ + backgroundColor:'#f7f8fa', + }, + '& .MuiDataGrid-row.Mui-selected:hover':{ + backgroundColor:'#EBEEF2', + }, + '& .MuiDataGrid-row.Mui-selected.Mui-hovered':{ + backgroundColor:'#EBEEF2', + }, + '& .MuiDataGrid-columnHeader:focus':{ + outline:'none'} + , + '& .MuiDataGrid-cell:focus':{ + outline:'none' + }, + '& .MuiDataGrid-cell.MuiDataGrid-cell--editing:focus-within':{ + outline:'none' + }, + '& .MuiDataGrid-row.Mui-hovered': { + backgroundColor:'#EBEEF2', + '.table-actions': { + visibility: 'visible' + } + }, + '& .MuiButtonBase-root.MuiIconButton-root.MuiIconButton-sizeSmall':{ + borderRadius:'4px' + }, + '& .MuiDataGrid-columnHeaderTitleContainer':{ + justifyContent:'space-between' + }, + '& .MuiOutlinedInput-input':{ + color:'#333', + fontSize:'14px' + } +} \ No newline at end of file diff --git a/frontend/packages/common/src/const/approval/const.tsx b/frontend/packages/common/src/const/approval/const.tsx new file mode 100644 index 00000000..fe7181b2 --- /dev/null +++ b/frontend/packages/common/src/const/approval/const.tsx @@ -0,0 +1,421 @@ +import {ProColumns} from "@ant-design/pro-components"; +import { ApprovalTableListItem, PublishTableListItem } from "./type"; +import { Tooltip } from "antd"; + + +export const TODO_LIST_COLUMN_NOT_INCLUDE_KEY:string[] = ['status','approver','approvalTime'] + +export const SUBSCRIBE_APPROVAL_TABLE_COLUMN : ProColumns[] = [ + { + title: '申请时间', + dataIndex: 'applyTime', + ellipsis:true, + width:182, + fixed:'left', + sorter: (a,b)=> { + return a.applyTime.localeCompare(b.applyTime) + }, + }, + { + title: '申请方-应用', + dataIndex: ['application','name'], + copyable: true, + ellipsis:true + }, + { + title: '申请服务', + dataIndex: ['service','name'], + copyable: true, + ellipsis:true + }, + { + title: '服务所属系统', + dataIndex: ['service','name'], + copyable: true, + ellipsis:true + }, + { + title: '服务所属团队', + dataIndex: ['team','name'], + copyable: true, + ellipsis:true + }, + { + title: '审批状态', + dataIndex: 'status', + valueType: 'text', + }, + { + title: '申请人', + dataIndex: ['applier','name'], + ellipsis: true, + width:88, + }, + { + title: '审批人', + dataIndex: ['approver','name'], + ellipsis: true, + width:88 + }, + { + title: '审批时间', + dataIndex: 'approvalTime', + ellipsis: true, + // sorter: true,, + width:182, + sorter: (a,b)=> { + return a.approvalTime.localeCompare(b.approvalTime) + }, + }, +]; + +export const SUBSCRIBE_APPROVAL_INNER_TODO_TABLE_COLUMN : ProColumns[] = [ + { + title: '申请时间', + dataIndex: 'applyTime', + // sorter: true, + copyable: true, + ellipsis:true, + width:182, + fixed:'left', + sorter: (a,b)=> { + return a.applyTime.localeCompare(b.applyTime) + }, + }, + { + title: '申请方-应用', + dataIndex: ['application','name'], + copyable: true, + ellipsis:true + }, + { + // title: '申请人', + title: 申请人, + dataIndex: ['applier','name'], + ellipsis: true, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true + }, + { + title: '申请服务', + dataIndex: ['service','name'], + copyable: true, + ellipsis:true + }, +]; + + +export const SUBSCRIBE_APPROVAL_INNER_DONE_TABLE_COLUMN : ProColumns[] = [ + { + title: '申请时间', + dataIndex: 'applyTime', + // sorter: true, + copyable: true, + ellipsis:true, + width:182, + fixed:'left', + sorter: (a,b)=> { + return a.applyTime.localeCompare(b.applyTime) + }, + }, + { + title: '申请方-应用', + dataIndex: ['application','name'], + copyable: true, + ellipsis:true + }, + { + // title: '申请人', + title: 申请人, + dataIndex: ['applier','name'], + ellipsis: true, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true, + }, + { + title: '申请服务', + dataIndex: ['service','name'], + copyable: true, + ellipsis:true + }, + { + title: '审批状态', + dataIndex: 'status', + valueType: 'select', + ellipsis: true, + filters: true, + onFilter: true, + valueEnum: new Map([ + [0, 拒绝], + [2,通过], + ]), + }, + { + title: '审批人', + dataIndex: ['approver','name'], + ellipsis: true, + width:88, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true, + }, + { + title: '审批时间', + dataIndex: 'approvalTime', + ellipsis: true, + // sorter: true,, + width:182, + sorter: (a,b)=> { + return a.approvalTime.localeCompare(b.approvalTime) + }, + }, +]; + +export type SubscribeApprovalTableListItem = { + applyTime: string; + id:string; + application:string; + service:string; + applier:string; + team?:string; + status:0|1; + approver:string; + approvalTime:string; +}; + + +export enum PublishApplyStatusEnum{ + 'accept'="审批完成", + 'apply'="发布审批中", + 'running'="在线", + 'none'="-", + 'refuse'="已拒绝", + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + 'close' = '-', + 'stop' = '中止', + 'error' = '发布异常', + 'publishing' = '发布中' +} + + +export const PublishTableStatusColorClass = { + 'success' : 'text-[#03a9f4]', + 'fail' : 'text-[#ff3b30]', + 'apply' : 'text-[#46BE11]', + 'refuse' : 'text-[#EF0020]', + 'running' : 'text-[#3D46F2]', + 'accept' : 'text-[#147AFE]', + 'none' : 'text-[var(--MAIN_TEXT)]', + 'approval' : 'text-[#03a9f4]', + 'done' : 'text-[#138913]', + 'stop' : 'text-[#ff3b30]', + 'close' : 'text-[var(--MAIN_TEXT)]', + 'error':'text-[#ff3b30]', + 'publishing':'text-[#46BE11]', +} + + +enum PublishStatusEnum{ + 'apply' = '待审批', + 'accept' = '审批通过', + 'done' = '已发布', + 'stop' = '发布终止', + 'close' = '已关闭', + 'refuse' = '已拒绝', + 'error' = '发布异常', + 'publishing' = '发布中' +} + +export const PUBLISH_APPROVAL_VERSION_INNER_TABLE_COLUMN : ProColumns[] = [ + { + title: '发布版本', + dataIndex: 'version', + copyable: true, + ellipsis:true, + width:160, + fixed:'left' + }, + { + title: '版本说明', + dataIndex: 'remark', + copyable: true, + ellipsis:true + }, + { + title: '创建版本时间', + dataIndex: 'createTime', + ellipsis:true, + sorter: (a,b)=> { + return a.createTime.localeCompare(b.createTime) + }, + }, + { + title: '版本状态', + dataIndex: 'status', + ellipsis:true, + filters: true, + onFilter: true, + valueType: 'select', + valueEnum:new Map([ + ['accept',{PublishApplyStatusEnum.accept || '-'}], + ['apply',{PublishApplyStatusEnum.apply || '-'}], + ['running',{PublishApplyStatusEnum.running || '-'}], + ['none',{PublishApplyStatusEnum.none || '-'}], + ['refuse',{PublishApplyStatusEnum.refuse || '-'}], + ['publishing',{PublishApplyStatusEnum.publishing || '-'}], + ['error',{PublishApplyStatusEnum.error || '-'}], + ]) + }, + { + title: '创建人', + dataIndex: ['creator','name'], + ellipsis: true, + width:88, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true, + } +]; + +export const PUBLISH_APPROVAL_RECORD_INNER_TABLE_COLUMN : ProColumns[] = [ + { + title: '申请时间', + dataIndex: 'applyTime', + copyable: true, + ellipsis:true, + width:182, + fixed:'left', + }, + { + title: '审核时间', + dataIndex: 'approveTime', + copyable: true, + ellipsis:true, + width:182, + }, + { + title: '版本号', + dataIndex: 'version', + copyable: true, + ellipsis:true + }, + { + title: '版本说明', + dataIndex: 'remark', + copyable: true, + ellipsis:true + }, + { + title: '发布状态', + dataIndex: 'status', + ellipsis:true, + valueEnum:new Map([ + ['apply',{PublishStatusEnum.apply || '-'}], + ['accept',{PublishStatusEnum.accept || '-'}], + ['done',{PublishStatusEnum.done || '-'}], + ['stop',{PublishStatusEnum.stop || '-'}], + ['close',{PublishStatusEnum.close || '-'}], + ['refuse',{PublishStatusEnum.refuse || '-'}], + ['publishing',{PublishStatusEnum.publishing || '-'}], + ['error',{PublishStatusEnum.error || '-'}], + ]) + }, + { + title: '备注', + dataIndex: 'comments', + ellipsis:true + }, + { + title: '申请人', + dataIndex: ['applicant','name'], + ellipsis: true, + width:88, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true, + }, + { + title: '审批人', + dataIndex: ['approver','name'], + ellipsis: true, + width:88, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true, + }, +]; + +export const PUBLISH_APPROVAL_TABLE_COLUMN : ProColumns[] = [ + { + title: '申请时间', + dataIndex: 'applyTime', + copyable: true, + ellipsis:true, + width:182, + fixed:'left', + sorter: (a,b)=> { + return a.applyTime.localeCompare(b.applyTime) + }, + }, + { + title: '申请系统', + dataIndex: ['service','name'], + copyable: true, + ellipsis:true + }, + { + title: '所属团队', + dataIndex: ['team','name'], + copyable: true, + ellipsis:true + }, + { + title: '审批状态', + dataIndex: 'status', + ellipsis:{ + showTitle:true + }, + filters: true, + onFilter: true, + valueType: 'select', + }, + { + title: '申请人', + dataIndex: ['applier','name'], + ellipsis: true, + width:88, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true, + }, + { + title: '审批人', + dataIndex: ['approver','name'], + ellipsis: true, + width:88, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true, + }, + { + title: '审批时间', + dataIndex: 'approvalTime', + // sorter: true, + ellipsis:true, + hideInSearch: true, + width:182, + sorter: (a,b)=> { + return a.approvalTime.localeCompare(b.approvalTime) + }, + }, +]; diff --git a/frontend/packages/common/src/const/approval/type.tsx b/frontend/packages/common/src/const/approval/type.tsx new file mode 100644 index 00000000..8e0a7d9f --- /dev/null +++ b/frontend/packages/common/src/const/approval/type.tsx @@ -0,0 +1,103 @@ +import { SystemInsidePublishOnlineItems } from "@core/pages/system/publish/SystemInsidePublishOnline"; +import { SystemReleaseStatus } from "@core/const/system/type"; +import { EntityItem } from "@common/const/type"; +import { SubscribeApprovalTableListItem, PublishApplyStatusEnum } from "./const"; + +export type SubscribeApprovalInfoType = { + applyTime: string; + id:string; + application:string; + applier:string; + service:string; + applyTeam:string; + team:string; + status:string; + approver:string; + approvalTime:string; + reason:string + opinion?:string +}; + + +export type PublishApprovalTableListItem = { + id:string; + applyTime:string; + service:string; + team:string; + status:string; + applier:string; + approver:string; + approvalTime:string; +}; + export type PublishApprovalApiItem = { + name:string + method:string + path:string + upstream:string + change:string + status:{ + upstreamStatus: SystemReleaseStatus, + docStatus: SystemReleaseStatus, + proxyStatus: SystemReleaseStatus} +} + +export type PublishApprovalUpstreamItem = { + upstream:EntityItem + cluster:EntityItem + type:'static'|'dynamic' + addr:string[] + change:'add'|'update'|'delete'|'none', + status:SystemReleaseStatus +} + +// 发布详情(版本) +export type PublishApprovalInfoType = { + id:string; + applyTime:string; + service:EntityItem; + applyTeam:EntityItem; + team:EntityItem; + status:string; + applier:EntityItem; + approver:EntityItem; + approvalTime:string; + areas:Array + remark:string + opinion?:string + diffs:{ + apis:PublishApprovalApiItem[] + upstreams:PublishApprovalUpstreamItem[] + } + clusterPublishStatus?:SystemInsidePublishOnlineItems[], + error:string +}; + + +export type ApprovalTableListItem = SubscribeApprovalTableListItem | PublishApprovalTableListItem + + +export type PublishVersionTableListItem = { + id:string, + version:string + service:EntityItem + remark:string + createTime:string + creator:EntityItem + status:keyof typeof PublishApplyStatusEnum + canRollback:boolean + canDelete:boolean + flowId:string +} + + +export type PublishTableListItem = { + id:string, + version:string + applyTime:string, + approveTime: string, + createTime:string, + creator: EntityItem, + service:EntityItem + team:EntityItem + status:keyof typeof PublishApplyStatusEnum +} \ No newline at end of file diff --git a/frontend/packages/common/src/const/code/const.ts b/frontend/packages/common/src/const/code/const.ts new file mode 100644 index 00000000..87f15a96 --- /dev/null +++ b/frontend/packages/common/src/const/code/const.ts @@ -0,0 +1,98 @@ +const CODE_LANG = [ + { + label: 'Java(OK HTTP)', + value: 20 + }, + { + label: 'PHP', + value: 9, + children: [ + { + label: 'pecl_http', + value: 10 + }, + { + label: 'cURL', + value: 11 + } + ] + }, + { + label: 'Python', + value: 12, + children: [ + { + label: 'http.client(Python 3)', + value: 13 + }, + { + label: 'Requests', + value: 14 + } + ] + }, + { + label: 'HTTP', + value: 1 + }, + { + label: 'cURL', + value: 2 + }, + { + label: 'JavaScript', + value: 3, + children: [ + { + label: 'Jquery AJAX', + value: 4 + }, + { + label: 'XHR', + value: 5 + } + ] + }, + { + label: 'NodeJS', + value: 6, + children: [ + { + label: 'Native', + value: 7 + }, + { + label: 'Request', + value: 8 + } + ] + }, + { + label: '微信小程序', + value: 21 + }, + // { + // label: 'Ruby(Net:Http)', + // value: 15 + // }, + { + label: 'Shell', + value: 16, + children: [ + { + label: 'Httpie', + value: 17 + }, + { + label: 'cUrl', + value: 18 + } + ] + }, + { + label: 'Go', + value: 19 + } +] + +export default CODE_LANG \ No newline at end of file diff --git a/frontend/packages/common/src/const/const.ts b/frontend/packages/common/src/const/const.ts new file mode 100644 index 00000000..b7937e72 --- /dev/null +++ b/frontend/packages/common/src/const/const.ts @@ -0,0 +1,20 @@ + +export type BasicResponse = { + code:number + data:T + msg:string +} + + +export const STATUS_CODE = { + SUCCESS:0, + UNANTHORIZED:401, + FORBIDDEN:403 +} + +export const STATUS_COLOR = { + 'done':'text-[#03a9f4]', + 'error':'text-[#ff3b30]' +} + +const NAV_HEIGHT = 72 \ No newline at end of file diff --git a/frontend/packages/common/src/const/domain/const.ts b/frontend/packages/common/src/const/domain/const.ts new file mode 100644 index 00000000..abaa0e9a --- /dev/null +++ b/frontend/packages/common/src/const/domain/const.ts @@ -0,0 +1,46 @@ +const DOMAIN_CONSTANT = [ + 'com', + 'cn', + 'xin', + 'net', + 'top', + 'xyz', + 'wang', + 'shop', + 'site', + 'club', + 'cc', + 'fun', + 'online', + 'biz', + 'red', + 'link', + 'ltd', + 'mobi', + 'info', + 'org', + 'name', + 'vip', + 'pro', + 'work', + 'tv', + 'kim', + 'group', + 'tech', + 'store', + 'ren', + 'ink', + 'pub', + 'live', + 'wiki', + 'design', + 'ai', + 'me', + 'io', + 'test', + 'example', + 'invalid', + 'localhost' +] + +export default DOMAIN_CONSTANT \ No newline at end of file diff --git a/frontend/packages/common/src/const/permissions.ts b/frontend/packages/common/src/const/permissions.ts new file mode 100644 index 00000000..f20b78c7 --- /dev/null +++ b/frontend/packages/common/src/const/permissions.ts @@ -0,0 +1,463 @@ +// denied - 禁用; granted - 拥有权限 +// 条件 anyOf/oneOf/anyOf/not +// 维度 backend - 后端的权限字段; + +export const PERMISSION_DEFINITION = [ + { + "system.organization.member.view": { + "granted": { + "anyOf": [{ "backend": ["system.organization.member.view"] }] + } + }, + "system.organization.member.add": { + "granted": { + "anyOf": [{ "backend": ["system.organization.member.manager"] }] + } + }, + "system.organization.member.edit": { + "granted": { + "anyOf": [{ "backend": ["system.organization.member.manager"] }] + } + }, + "system.organization.member.remove": { + "granted": { + "anyOf": [{ "backend": ["system.organization.member.manager"] }] + } + }, + "system.organization.member.delete": { + "granted": { + "anyOf": [{ "backend": ["system.organization.member.manager"] }] + } + }, + "system.organization.member.block": { + "granted": { + "anyOf": [{ "backend": ["system.organization.member.manager"] }] + } + }, + "system.organization.member.department.add": { + "granted": { + "anyOf": [{ "backend": ["system.organization.member.manager"] }] + } + }, + "system.organization.member.department.edit": { + "granted": { + "anyOf": [{ "backend": ["system.organization.member.manager"] }] + } + }, + "system.organization.member.department.delete": { + "granted": { + "anyOf": [{ "backend": ["system.organization.member.manager"] }] + } + }, + "system.organization.team.view": { + "granted": { + "anyOf": [{ "backend": ["system.organization.team.view"] }] + } + }, + "system.organization.team.add": { + "granted": { + "anyOf": [{ "backend": ["system.organization.team.manager"] }] + } + }, + "system.organization.team.edit": { + "granted": { + "anyOf": [{ "backend": ["system.organization.team.manager"] }] + } + }, + "system.organization.team.delete": { + "granted": { + "anyOf": [{ "backend": ["system.organization.team.manager"] }] + } + }, + "system.organization.team.running": { + "granted": { + "anyOf": [{ "backend": ["system.organization.team.manager"] }] + } + }, + "system.organization.role.view": { + "granted": { + "anyOf": [{ "backend": ["system.organization.role.view_system_role","system.organization.role.view_team_role"] }] + } + }, + "system.organization.role.system.view": { + "granted": { + "anyOf": [{ "backend": ["system.organization.role.view_system_role"] }] + } + }, + "system.organization.role.system.add": { + "granted": { + "anyOf": [{ "backend": ["system.organization.role.manager_system_role"] }] + } + }, + "system.organization.role.system.edit": { + "granted": { + "anyOf": [{ "backend": ["system.organization.role.manager_system_role"] }] + } + }, + "system.organization.role.system.delete": { + "granted": { + "anyOf": [{ "backend": ["system.organization.role.manager_system_role"] }] + } + }, + "system.organization.role.team.view": { + "granted": { + "anyOf": [{ "backend": ["system.organization.role.view_team_role"] }] + } + }, + "system.organization.role.team.add": { + "granted": { + "anyOf": [{ "backend": ["system.organization.role.manager_team_role"] }] + } + }, + "system.organization.role.team.edit": { + "granted": { + "anyOf": [{ "backend": ["system.organization.role.manager_team_role"] }] + } + }, + "system.organization.role.team.delete": { + "granted": { + "anyOf": [{ "backend": ["system.organization.role.manager_team_role"] }] + } + }, + "system.api_market.service_classification.view": { + "granted": { + "anyOf": [{ "backend": ["system.api_market.service_classification.view"] }] + } + }, + "system.api_market.service_classification.add": { + "granted": { + "anyOf": [{ "backend": ["system.api_market.service_classification.manager"] }] + } + }, + "system.api_market.service_classification.edit": { + "granted": { + "anyOf": [{ "backend": ["system.api_market.service_classification.manager"] }] + } + }, + "system.api_market.service_classification.delete": { + "granted": { + "anyOf": [{ "backend": ["system.api_market.service_classification.manager"] }] + } + }, + "system.devops.cluster.view": { + "granted": { + "anyOf": [{ "backend": ["system.devops.cluster.view"] }] + } + }, + "system.devops.cluster.add": { + "granted": { + "anyOf": [{ "backend": ["system.devops.cluster.manager"] }] + } + }, + "system.devops.cluster.edit": { + "granted": { + "anyOf": [{ "backend": ["system.devops.cluster.manager"] }] + } + }, + "system.devops.cluster.delete": { + "granted": { + "anyOf": [{ "backend": ["system.devops.cluster.manager"] }] + } + }, + "system.devops.ssl_certificate.view": { + "granted": { + "anyOf": [{ "backend": ["system.devops.ssl_certificate.view"] }] + } + }, + "system.devops.ssl_certificate.add": { + "granted": { + "anyOf": [{ "backend": ["system.devops.ssl_certificate.manager"] }] + } + }, + "system.devops.ssl_certificate.edit": { + "granted": { + "anyOf": [{ "backend": ["system.devops.ssl_certificate.manager"] }] + } + }, + "system.devops.ssl_certificate.delete": { + "granted": { + "anyOf": [{ "backend": ["system.devops.ssl_certificate.manager"] }] + } + }, + "system.devops.log_configuration.view": { + "granted": { + "anyOf": [{ "backend": ["system.devops.log_configuration.view"] }] + } + }, + "system.devops.log_configuration.add": { + "granted": { + "anyOf": [{ "backend": ["system.devops.log_configuration.manager"] }] + } + }, + "system.devops.log_configuration.edit": { + "granted": { + "anyOf": [{ "backend": ["system.devops.log_configuration.manager"] }] + } + }, + "system.devops.log_configuration.publish": { + "granted": { + "anyOf": [{ "backend": ["system.devops.log_configuration.manager"] }] + } + }, + "system.devops.log_configuration.delete": { + "granted": { + "anyOf": [{ "backend": ["system.devops.log_configuration.manager"] }] + } + }, + "system.workspace.application.view_all": { + "granted": { + "anyOf": [{ "backend": ["system.workspace.application.view_all"] }] + } + }, + "system.workspace.service.view_all": { + "granted": { + "anyOf": [{ "backend": ["system.workspace.service.view_all"] }] + } + }, + "system.workspace.team.view_all": { + "granted": { + "anyOf": [{ "backend": ["system.workspace.team.view_all"] }] + } + }, + "system.workspace.api_market.view": { + "granted": { + "anyOf": [{ "backend": ["system.workspace.api_market.view"] }] + } + }, + "team.service.api.view": { + "granted": { + "anyOf": [{ "backend": ["team.service.api.view"] }] + } + }, + "team.service.api.add": { + "granted": { + "anyOf": [{ "backend": ["team.service.api.manager"] }] + } + }, + "team.service.api.edit": { + "granted": { + "anyOf": [{ "backend": ["team.service.api.manager"] }] + } + }, + "team.service.api.copy": { + "granted": { + "anyOf": [{ "backend": ["team.service.api.manager"] }] + } + }, + "team.service.api.delete": { + "granted": { + "anyOf": [{ "backend": ["team.service.api.manager"] }] + } + }, + "team.service.api.import": { + "granted": { + "anyOf": [{ "backend": ["team.service.api.manager"] }] + } + }, + "team.service.upstream.view": { + "granted": { + "anyOf": [{ "backend": ["team.service.upstream.view"] }] + } + }, + "team.service.upstream.add": { + "granted": { + "anyOf": [{ "backend": ["team.service.upstream.manager"] }] + } + }, + "team.service.upstream.edit": { + "granted": { + "anyOf": [{ "backend": ["team.service.upstream.manager"] }] + } + }, + "team.service.upstream.delete": { + "granted": { + "anyOf": [{ "backend": ["team.service.upstream.manager"] }] + } + }, + "team.service.release.view": { + "granted": { + "anyOf": [{ "backend": ["team.service.release.view"] }] + } + }, + "team.service.release.add": { + "granted": { + "anyOf": [{ "backend": ["team.service.release.manager"] }] + } + }, + "team.service.release.online": { + "granted": { + "anyOf": [{ "backend": ["team.service.release.manager"] }] + } + }, + "team.service.release.stop": { + "granted": { + "anyOf": [{ "backend": ["team.service.release.manager"] }] + } + }, + "team.service.release.cancel": { + "granted": { + "anyOf": [{ "backend": ["team.service.release.manager"] }] + } + }, + "team.service.release.rollback": { + "granted": { + "anyOf": [{ "backend": ["team.service.release.manager"] }] + } + }, + "team.service.release.delete": { + "granted": { + "anyOf": [{ "backend": ["team.service.release.manager"] }] + } + }, + "team.service.release.approval": { + "granted": { + "anyOf": [{ "backend": ["team.service.release.manager"] }] + } + }, + "team.service.subscription.view": { + "granted": { + "anyOf": [{ "backend": ["team.service.subscription.view"] }] + } + }, + "team.service.subscription.approval": { + "granted": { + "anyOf": [{ "backend": ["team.service.subscription.manager"] }] + } + }, + "team.service.subscription.add": { + "granted": { + "anyOf": [{ "backend": ["team.service.subscription.manager"] }] + } + }, + "team.service.subscription.delete": { + "granted": { + "anyOf": [{ "backend": ["team.service.subscription.manager"] }] + } + }, + "team.service.service.view": { + "granted": { + "anyOf": [{ "backend": [""] }] + } + }, + "team.service.service.add": { + "granted": { + "anyOf": [{ "backend": ["team.service.service.manager"] }] + } + }, + "team.service.service.edit": { + "granted": { + "anyOf": [{ "backend": ["team.service.service.manager"] }] + } + }, + "team.service.service.delete": { + "granted": { + "anyOf": [{ "backend": ["team.service.service.manager"] }] + } + }, + "team.application.subscription.view": { + "granted": { + "anyOf": [{ "backend": ["team.application.subscription.view"] }] + } + }, + "team.application.subscription.add": { + "granted": { + "anyOf": [{ "backend": ["team.application.subscription.manager"] }] + } + }, + "team.application.subscription.edit": { + "granted": { + "anyOf": [{ "backend": ["team.application.subscription.manager"] }] + } + }, + "team.application.subscription.delete": { + "granted": { + "anyOf": [{ "backend": ["team.application.subscription.manager"] }] + } + }, + "team.application.application.view": { + "granted": { + "anyOf": [{ "backend": ["team.application.application.view"] }] + } + }, + "team.application.application.add": { + "granted": { + "anyOf": [{ "backend": ["team.application.application.manager"] }] + } + }, + "team.application.application.edit": { + "granted": { + "anyOf": [{ "backend": ["team.application.application.manager"] }] + } + }, + "team.application.application.delete": { + "granted": { + "anyOf": [{ "backend": ["team.application.application.manager"] }] + } + }, + "team.application.authorization.view": { + "granted": { + "anyOf": [{ "backend": ["team.application.authorization.view"] }] + } + }, + "team.application.authorization.add": { + "granted": { + "anyOf": [{ "backend": ["team.application.authorization.manager"] }] + } + }, + "team.application.authorization.edit": { + "granted": { + "anyOf": [{ "backend": ["team.application.authorization.manager"] }] + } + }, + "team.application.authorization.delete": { + "granted": { + "anyOf": [{ "backend": ["team.application.authorization.manager"] }] + } + }, + "team.team.team.view": { + "granted": { + "anyOf": [{ "backend": ["team.team.team.view"] }] + } + }, + "team.team.team.edit": { + "granted": { + "anyOf": [{ "backend": ["team.team.team.manager"] }] + } + }, + "team.team.member.view": { + "granted": { + "anyOf": [{ "backend": ["team.team.member.view"] }] + } + }, + "team.team.member.add": { + "granted": { + "anyOf": [{ "backend": ["team.team.member.manager"] }] + } + }, + "team.team.member.edit": { + "granted": { + "anyOf": [{ "backend": ["team.team.member.manager"] }] + } + }, + "project.mySystem.topology.view": { + "granted": { + "anyOf": [{ "backend": ["project.subscribe_approval"] }] + } + }, + "project.mySystem.access.view": { + "granted": { + "anyOf": [{ "backend": ["project.permission_manager"] }] + } + }, + "project.mySystem.access.edit": { + "granted": { + "anyOf": [{ "backend": ["project.permission_manager"] }] + } + }, + "project.mySystem.access.delete": { + "granted": { + "anyOf": [{ "backend": ["project.permission_manager"] }] + } + } + } + ]; \ No newline at end of file diff --git a/frontend/packages/common/src/const/permissions.yaml b/frontend/packages/common/src/const/permissions.yaml new file mode 100644 index 00000000..af742507 --- /dev/null +++ b/frontend/packages/common/src/const/permissions.yaml @@ -0,0 +1,199 @@ +system: + - name: organization + cname: '组织管理' + value: 'organization' + children: + - name: member + cname: '成员' + value: 'member' + access: + - system.organization.member.view + - system.organization.member.add + - system.organization.member.edit + - system.organization.member.delete + - system.organization.member.block + - system.organization.member.department.add + - system.organization.member.department.edit + - system.organization.member.department.delete + - name: team_manager + cname: '团队管理' + desc: '团队管理' + - system.organization.team.view + - system.organization.team.add + - system.organization.team.edit + - system.organization.team.delete + - system.organization.team.running + - name: role_manager + cname: '角色管理' + desc: '角色管理' + - system.organization.role.view + - system.organization.role.system.view + - system.organization.role.system.add + - system.organization.role.system.edit + - system.organization.role.system.delete + - system.organization.role.team.view + - system.organization.role.team.add + - system.organization.role.team.edit + - system.organization.role.team.delete + - name: API Market + cname: 'API市场' + value: 'api_market' + children: + - name: service classification + cname: '服务分类' + value: 'service_classification' + children: + - system.api_market.service_classification.view + - system.api_market.service_classification.add + - system.api_market.service_classification.edit + - system.api_market.service_classification.delete + - name: devops + cname: 运维 + value: 'devops' + children: + - name: cluster + cname: 集群 + value: 'cluster' + children: + - system.devops.cluster.view + - system.devops.cluster.add + - system.devops.cluster.edit + - system.devops.cluster.delete + - name: ssl certificate + cname: 证书 + value: 'ssl_certificate' + children: + - system.devops.ssl_certificate.view + - system.devops.ssl_certificate.add + - system.devops.ssl_certificate.edit + - system.devops.ssl_certificate.delete + - name: log configuration + cname: 日志 + value: 'log_configuration' + children: + - system.devops.log_configuration.view + - system.devops.log_configuration.add + - system.devops.log_configuration.edit + - system.devops.log_configuration.publish + - system.devops.log_configuration.delete + - name: workspace + cname: 工作空间 + value: 'workspace' + children: + - name: application + cname: 应用 + value: 'application' + children: + - system.workspace.application.view_all + - name: service + cname: 服务 + value: 'service' + children: + - system.workspace.service.view_all + - name: team + cname: 团队 + value: 'team' + children: + - system.workspace.team.view_all + - name: api market + cname: API市场 + value: 'api_market' + children: + - system.workspace.api_market.view +team: + - name: service + cname: 服务 + value: 'service' + children: + - name: api + cname: API + value: 'api' + children: + - team.service.api.view + - team.service.api.add + - team.service.api.edit + - team.service.api.copy + - team.service.api.delete + - team.service.api.import + - name: upstream + cname: 上游 + value: 'upstream' + children: + - team.service.upstream.view + - team.service.upstream.add + - team.service.upstream.edit + - team.service.upstream.delete + - name: release + cname: 发布 + value: 'release' + children: + - team.service.release.view + - team.service.release.add + - team.service.release.rollback + - team.service.release.delete + - team.service.release.approval + - team.service.release.online + - team.service.release.cancel + - team.service.release.stop + - name: subscription management + cname: 订阅方管理 + value: 'subscription' + children: + - team.service.subscription.view + - team.service.subscription.approval + - team.service.subscription.add + - team.service.subscription.delete + - name: service + cname: 服务管理 + value: 'service' + children: + - team.service.service.view + - team.service.service.add + - team.service.service.edit + - team.service.service.delete + - name: application + cname: 应用 + value: 'application' + children: + - name: subscription Service + cname: 订阅服务 + value: 'subscription' + children: + - team.application.subscription.view + - team.application.subscription.add + - team.application.subscription.edit + - team.application.subscription.delete + - name: authorization + cname: 访问授权 + value: 'authorization' + children: + - team.application.authorization.view + - team.application.authorization.manager + - team.application.authorization.add + - team.application.authorization.edit + - team.application.authorization.delete + - name: application + cname: 应用 + value: 'application' + children: + - team.application.application.view + - team.application.application.add + - team.application.application.edit + - team.application.application.delete + - name: team + cname: 团队 + value: 'team' + children: + - name: member + cname: 成员 + value: 'member' + children: + - team.team.member.view + - team.team.member.add + - team.team.member.edit + - name: team + cname: 团队管理 + value: 'team' + children: + - team.team.team.view + - team.team.team.edit diff --git a/frontend/packages/common/src/const/type.ts b/frontend/packages/common/src/const/type.ts new file mode 100644 index 00000000..9f9c4f7e --- /dev/null +++ b/frontend/packages/common/src/const/type.ts @@ -0,0 +1,101 @@ +import { PERMISSION_DEFINITION } from "./permissions" +import { MatchPositionEnum, MatchTypeEnum } from "@core/const/system/const" + +export type UserInfoType = { + username: string + nickname: string + email: string + phone: string + avatar: string +} + +export type UserProfileProps = { + entity?:UserInfoType +} + +export type UserProfileHandle = { + save:()=>Promise +} + +export type ClusterSimpleOption = { + id:string + name:string + description:string +} + + +export type ClusterEnumData = { + name:string, + uuid:string, + title:string +} + +export interface ClusterEnum{ + clusters:Array + name:string +} + +export type TeamSimpleMemberItem = { + user:EntityItem + mail:string + department:EntityItem +} + +export type MemberItem = { + id:string; + name:string; + email:string; + department:Array<{id:string,name:string}> +} + +export type DashboardPartitionItem = { + id:string; + name:string + enableMonitor:boolean +} + + +export type SimpleTeamItem = { + id:string + name:string + description:string + appNum:number +} + +export type MatchItem = { + position:MatchPositionEnum + matchType:MatchTypeEnum + key:string + pattern:string + id?:string +} + +export type EntityItem = { + id:string + name:string +} + +export type DynamicMenuItem = { + name:string + title:string + path:string +} + +export type AccessDataType = keyof typeof PERMISSION_DEFINITION[0] + + + +export type NewSimpleMemberItem = { + user:EntityItem + email:string + department:string + avatar:string +} + +export type SimpleMemberItem = { + id:string + name:string + email:string + department:string + avatar:string +} \ No newline at end of file diff --git a/frontend/packages/common/src/contexts/BreadcrumbContext.tsx b/frontend/packages/common/src/contexts/BreadcrumbContext.tsx new file mode 100644 index 00000000..8cf12b58 --- /dev/null +++ b/frontend/packages/common/src/contexts/BreadcrumbContext.tsx @@ -0,0 +1,26 @@ +import {createContext, useContext, useState} from "react"; +import {BreadcrumbItemType} from "antd/es/breadcrumb/Breadcrumb"; + +interface BreadcrumbContextType { + breadcrumb: BreadcrumbItemType[]; + setBreadcrumb: (newItems: BreadcrumbItemType[]) => void; +} + +const BreadcrumbContext = createContext(undefined); + +export const useBreadcrumb = () => { + const context = useContext(BreadcrumbContext); + if (!context) { + throw new Error('useBreadcrumb must be used within a BreadcrumbProvider'); + } + return context; +}; +export const BreadcrumbProvider = ({children}:unknown) =>{ + const [breadcrumb,setBreadcrumb] = useState([]) + + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/frontend/packages/common/src/contexts/GlobalStateContext.tsx b/frontend/packages/common/src/contexts/GlobalStateContext.tsx new file mode 100644 index 00000000..287bf3c2 --- /dev/null +++ b/frontend/packages/common/src/contexts/GlobalStateContext.tsx @@ -0,0 +1,182 @@ +import {createContext, Dispatch, FC, ReactNode, useContext, useReducer, useState} from "react"; +import { useFetch } from "@common/hooks/http"; +import { App } from "antd"; +import { BasicResponse, STATUS_CODE } from "@common/const/const"; +import { checkAccess } from "@common/utils/permission"; +import { PERMISSION_DEFINITION } from "@common/const/permissions"; + +interface GlobalState { + isAuthenticated: boolean; + userData: UserData | null; + version: string; + updateDate: string; + powered:string; + mainPage:string +} + +// Define the shape of the user data +interface UserData { + username: string; + // Add other user-related fields as needed +} + +// Define actions for state updates +type Action = + | { type: 'LOGIN'} + | { type: 'LOGOUT' } + | { type: 'UPDATE_USERDATA'; userData: UserData } + | { type: 'UPDATE_VERSION'; version: string } + | { type: 'UPDATE_DATE'; updateDate: string } + | { type: 'UPDATE_POWER'; powered: string } + | { type: 'UPDATE_MAIN_PAGE'; mainPage: string }; + +/* + 存储用户登录、信息、权限等数据 +*/ +const GlobalContext = createContext<{ + state: GlobalState; + dispatch: Dispatch; + accessData:Map; + pluginAccessDictionary:{[k:string]:string}; + getGlobalAccessData:()=>void; + getTeamAccessData:(teamId:string)=>void; + getPluginAccessDictionary:(pluginData:{[k:string]:string})=>void + resetAccess:()=>void + cleanTeamAccessData:()=>void + checkPermission:(access:keyof typeof PERMISSION_DEFINITION[0] | Array)=>boolean + teamDataFlushed:boolean + accessInit:boolean + +} | undefined>(undefined); + +// Define a reducer function to handle state updates +const globalReducer = (state: GlobalState, action: Action): GlobalState => { + switch (action.type) { + case 'LOGIN': + return { + ...state, + isAuthenticated: true, + }; + case 'LOGOUT': + return { + ...state, + isAuthenticated: false, + userData: null, + } + case 'UPDATE_USERDATA': + return { + ...state, + userData: action.userData, + }; + case 'UPDATE_VERSION': + return { + ...state, + version: action.version, + }; + case 'UPDATE_DATE': + return { + ...state, + updateDate: action.updateDate, + }; + case 'UPDATE_POWER': + return { + ...state, + powered: action.powered, + }; + case 'UPDATE_MAIN_PAGE': + return { + ...state, + mainPage: action.mainPage, + }; + default: + return state; + } +}; + +// Create a context provider component +export const GlobalProvider: FC<{children:ReactNode}> = ({ children }) => { + const {fetchData} = useFetch() + const { message } = App.useApp() + const [state, dispatch] = useReducer(globalReducer, { + isAuthenticated: true, //mock用 + userData: null, + version: '1.0.0', + updateDate: '2024-07-01', + powered:'Powered by https://apipark.com', + mainPage:'/service/list' + }); + const [accessData,setAccessData] = useState>(new Map()) + const [pluginAccessDictionary, setPluginAccessDictionary] = useState<{[k:string]:string}>({}) + const [teamDataFlushed, setTeamDataFlushed] = useState(false) + const [accessInit, setAccessInit] = useState(false) + + const getGlobalAccessData = ()=>{ + fetchData>('profile/permission/system',{method:'GET'},).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setAccessInit(true) + setAccessData(prevData => new Map(prevData).set('system', data.access)) + }else{ + message.error(msg || '操作失败') + } + }) + } + + const getTeamAccessData = (teamId:string)=>{ + fetchData>('profile/permission/team',{method:'GET',eoParams:{team:teamId}},).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setAccessData(prevData => new Map(prevData).set('team', data.access)) + setTeamDataFlushed(true) + }else{ + message.error(msg || '操作失败') + } + }) + } + + const cleanTeamAccessData = ()=>{ + setTeamDataFlushed(false) + setAccessData(prevData => prevData.set('team',[])) + } + + + const getPluginAccessDictionary = (pluginData:{[k:string]:string})=>{ + setPluginAccessDictionary(pluginData) + } + + const resetAccess = ()=>{ + setAccessData(new Map()) + setPluginAccessDictionary({}) + } + + const checkPermission = (access:keyof typeof PERMISSION_DEFINITION[0] | Array)=>{ + let revs = false; + if (Array.isArray(access)) { + revs = access.some(item => checkAccess(item, accessData)); + } else { + revs = checkAccess(access, accessData); + } + return revs + } + + + return ( + + {children} + + ); +}; + +// Create a custom hook for accessing the global context +export const useGlobalContext = () => { + const context = useContext(GlobalContext); + if (!context) { + throw new Error('useGlobalContext must be used within a GlobalProvider'); + } + return context; +}; \ No newline at end of file diff --git a/frontend/packages/common/src/hooks/copy.ts b/frontend/packages/common/src/hooks/copy.ts new file mode 100644 index 00000000..fff54b98 --- /dev/null +++ b/frontend/packages/common/src/hooks/copy.ts @@ -0,0 +1,68 @@ +/* + * @Name: + * @Description: + * @Copyright: 广州银云信息科技有限公司 + * @LastEditors: maggieyyy + * @LastEditTime: 2024-05-10 16:38:56 + */ +import { message } from 'antd'; +import { useEffect, useState } from 'react'; + +const useCopyToClipboard = () => { + const [isCopied, setIsCopied] = useState(false); + + const copyToClipboard = (text: string) => { + if (navigator.clipboard && window.isSecureContext) { + navigator.clipboard.writeText(text)?.then(() => { + message.success('复制成功') + setIsCopied(true) + }) + .catch((error) => { + console.error('Failed to copy text to clipboard:', error); + }); + } else { + // 创建text partition + const textArea = document.createElement("textarea"); + textArea.value = text; + // 使text area不在viewport,同时设置不可见 + textArea.style.position = "absolute"; + textArea.style.opacity = 0 + ''; + textArea.style.left = "-999999px"; + textArea.style.top = "-999999px"; + document.body.appendChild(textArea); + // textArea.focus(); + textArea.select(); + new Promise((resolve, reject) => { + if(document.execCommand('copy')) { + message.success('复制成功') + setIsCopied(true) + resolve() + } else { + reject('Failed to copy text to clipboard:') + } + }).catch((error) => { + console.error('Failed to copy text to clipboard:', error); + }).finally(() => { + textArea.remove(); + + }) + } + }; + + + useEffect(() => { + if (isCopied) { + const timeout = setTimeout(() => { + setIsCopied(false); + }, 3000); + + return () => { + clearTimeout(timeout); + }; + } + }, [isCopied]); + + return { isCopied, copyToClipboard }; +}; + +export default useCopyToClipboard; diff --git a/frontend/packages/common/src/hooks/crypto.ts b/frontend/packages/common/src/hooks/crypto.ts new file mode 100644 index 00000000..3943f9f6 --- /dev/null +++ b/frontend/packages/common/src/hooks/crypto.ts @@ -0,0 +1,26 @@ +/* + * @Date: 2024-01-31 15:00:11 + * @LastEditors: maggieyyy + * @LastEditTime: 2024-05-10 17:03:03 + * @FilePath: \frontend\packages\core\src\hooks\crypto.ts + */ +// import CryptoJS from 'crypto-js'; + +// export const useCrypto = () => { +// const key = '1e42=7838a1vfc6n'; + +// const encryptByEnAES = (secretKey: string, data: string, initializationVector?: string): string => { +// const iv = CryptoJS.enc.Latin1.parse(initializationVector || key); +// const keyForEncryption = CryptoJS.enc.Latin1.parse(CryptoJS.MD5(secretKey).toString()); + +// const cipher = CryptoJS.AES.encrypt(data, keyForEncryption, { +// iv, +// mode: CryptoJS.mode.CBC, +// padding: CryptoJS.pad.Pkcs7, +// }); + +// return CryptoJS.enc.Base64.stringify(cipher.ciphertext); +// }; + +// return { encryptByEnAES }; +// }; \ No newline at end of file diff --git a/frontend/packages/common/src/hooks/excel.ts b/frontend/packages/common/src/hooks/excel.ts new file mode 100644 index 00000000..c53a7d76 --- /dev/null +++ b/frontend/packages/common/src/hooks/excel.ts @@ -0,0 +1,59 @@ + +import * as ExcelJS from 'exceljs'; +import { saveAs } from 'file-saver'; +import { ProColumnType } from '@ant-design/pro-components'; + +export const useExcelExport = () => { + + const createExcel = (sheetTitle: string, columns: ExcelJS.Column[], tableData: T[]) => { + const workBook = new ExcelJS.Workbook() + const sheet = workBook.addWorksheet(sheetTitle || '默认工作表'); + sheet.columns = columns; + sheet.addRows(tableData); + return workBook + }; + + const exportExcel = async (fileTitle: string, date: [number, number], sheetTitle: string, tableId: string, tableColumnConfig: (ProColumnType&{eoTitle:string})[], tableData: T[]) => { + const workBook = createExcel(sheetTitle, getColumns(tableId, tableColumnConfig) as ExcelJS.Column[], tableData || []) + const fileName = getFileName(fileTitle, date); + try { + const buffer = await workBook.xlsx.writeBuffer(); + saveAs(new Blob([buffer], { + type: 'application/octet-stream' + }), `${fileName}.xlsx`); + } catch (error) { + console.error('Error exporting Excel file:', error); + } + }; + + const getColumns = (tableId: string, tableColumnConfig: (ProColumnType&{eoTitle:string})[]) => { + let tableConfig: Record | null; + try { + const storedConfig = localStorage.getItem(tableId); + tableConfig = storedConfig ? JSON.parse(storedConfig) : {}; + } catch (error) { + console.error('Error parsing localStorage config:', error); + tableConfig = {}; + } + return tableColumnConfig + .filter((head: ProColumnType&{eoTitle:string}) => tableConfig?.[head.dataIndex as string]?.show) + .map((head) => { return({ + header: head.eoTitle, + key: head.dataIndex, + width: (head.eoTitle as string).length > 5 ? (head.eoTitle as string).length * 3 : 15, + style: (head.dataIndex as string).includes('Rate') ? { numFmt: '0.00%' } : undefined, + })}); + }; + + const getFileName = (fileTitle: string, date: [number, number]): string => { + const [start, end] = date.map((time) => getDateFormat(time)); + return `${fileTitle}-${start}至${end}`; + }; + + const getDateFormat = (time: number): string => { + const date = new Date(time * 1000); + return `${date.getFullYear()}${(date.getMonth() + 1).toString().padStart(2, '0')}${date.getDate().toString().padStart(2, '0')}-${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; + }; + + return { exportExcel }; +}; diff --git a/frontend/packages/common/src/hooks/http.ts b/frontend/packages/common/src/hooks/http.ts new file mode 100644 index 00000000..9b433238 --- /dev/null +++ b/frontend/packages/common/src/hooks/http.ts @@ -0,0 +1,200 @@ +import { STATUS_CODE } from "@common/const/const"; + +const urlWhiteList = [/api.example.com\/users/, /api.example2.com\/products/]; // 正则白名单 + +function shouldNotTransform(url:string) { + return urlWhiteList.some(regex => regex.test(url)); +} + +function toCamel(s:string) { + return s.replace(/(_\w)/g, k => k[1].toUpperCase()); +} + +function toSnake(s:string) { + return s.replace(/([A-Z])/g, '_$1').toLowerCase(); +} + +function isObject(obj:unknown) { + return obj === Object(obj) && !Array.isArray(obj) && typeof obj !== 'function'; +} + +// 将对象的键从下划线转为驼峰 +function keysToCamel(o:unknown,transformKeys:string[]):unknown { + if (isObject(o)) { + const n:{[k:string]:unknown} = {}; + Object.keys(o as object).forEach(k => { + const newKey = transformKeys.includes(k) ? toCamel(k) : k; + n[newKey] = keysToCamel((o as {[k:string]:unknown})[k],transformKeys); + }); + return n; + } + else if (Array.isArray(o)) { + return o.map(i => keysToCamel(i,transformKeys)); + } + return o; +} + +// 将对象的键从驼峰转为下划线 +function keysToSnake(o:unknown,transformKeys:string[]):unknown { + if (isObject(o)) { + const n:{[k:string]:unknown} = {}; + Object.keys(o as object).forEach(k => { + const newKey = transformKeys.includes(k) ? toSnake(k) : k; + n[newKey] = keysToSnake((o as {[k:string]:unknown})[k],transformKeys); + }); + return n; + }else if (Array.isArray(o)) { + return o.map(i => keysToSnake(i,transformKeys)); + } + return o; +} + +// 将查询字符串的键从驼峰转换为下划线 +function convertQueryParamsToSnake( + params: { [k: string]: unknown }, + shouldTransformKeys: boolean, + transformKeys: string[] + ) { + const newParams = new URLSearchParams(); + + for (const key in params) { + if (shouldTransformKeys && transformKeys?.includes(key)) { + const newKey = toSnake(key); + const value = params[key]; + appendParam(newParams, newKey, value as Array | string); + } else { + appendParam(newParams, key, params[key] as Array | string); + } + } + + return newParams; + } + + function appendParam(params: URLSearchParams, key: string, value: Array | string) { + if (value !== undefined && value !== null) { + if (Array.isArray(value)) { + value.forEach((item) => params.append(key, item)); + } else { + params.append(key, value as string); + } + } + } + + +function isJsonHttp(headers: Headers | {[k:string]:string}): boolean { + const contentType = headers instanceof Headers ? headers.get('Content-Type') : headers['Content-Type']; + return contentType?.includes('application/json') ?? false; +} + +const trimStringValuesInObject = (obj: unknown): unknown => { + if (typeof obj === 'string') { + return (obj as string).trim(); + } + if (Array.isArray(obj)) { + return obj.map(trimStringValuesInObject); + } + if (typeof obj === 'object' && obj !== null) { + return Object.fromEntries( + Object.entries(obj).map(([key, value]) => [key, trimStringValuesInObject(value)]) + ); + } + return obj; +} + +const processQueryParams = (url: string, options: EoRequest, shouldTransformKeys: boolean) => { + if (options.eoParams) { + const cleanParams = Object.fromEntries( + Object.entries(options.eoParams) + .filter(([, value]) => value !== undefined) + .map(([key, value]) => [key, typeof value === 'string' ? value.trim() : value]) + ); + const queryParams = convertQueryParamsToSnake(cleanParams, shouldTransformKeys, options.eoTransformKeys as string[]); + const queryString = queryParams.toString(); + url += (url.includes('?') ? '&' : '?') + queryString; // 添加查询字符串到URL + } + return url; +} + + +const processRequestBody = (options: EoRequest, headers: EoHeaders, shouldTransformKeys: boolean) => { + let newBody:{[k:string]:unknown}|undefined + if (shouldTransformKeys && isJsonHttp(headers) && options.eoBody) { + newBody = keysToSnake(options.eoBody, options.eoTransformKeys as string[]) as {[k:string]:unknown}; + } + + if (isJsonHttp(headers) && (newBody || options.eoBody)) { + options.body = JSON.stringify(trimStringValuesInObject(newBody || options.eoBody)); + } + return options.body; +} + +const DEFAULT_HEADERS = { + 'Content-Type': 'application/json', + namespace:'default' +}; + +type EoRequest = RequestInit & {eoParams?:{[k:string]:unknown},eoTransformKeys?:string[],eoApiPrefix?:string,eoBody?:{[k:string]:unknown}|Array|string} + +type EoHeaders = Headers | {[k:string]:string} + +export function useFetch(){ + function fetchData(url:string, options: EoRequest ) { + // 合并传入的headers与默认headers + const headers = { ...DEFAULT_HEADERS, ...options.headers }; + + // 检查是否需要转换键 + const shouldTransformKeys = !shouldNotTransform(url) && options?.eoTransformKeys && options?.eoTransformKeys?.length > 0; + + // 处理URL查询参数 + url = processQueryParams(url, options, !!shouldTransformKeys); + + // 处理请求体, 当请求头为json时,fetch的body应当是json字符串 + options.body = processRequestBody(options, headers as EoHeaders, !!shouldTransformKeys); + // 全局请求前拦截 + const finalOptions = { + ...(options || {}), + headers: { + ...headers + // Authorization: 'Bearer your-token', // 示例:添加统一的Token认证 + }, + }; + + return fetch(`${options?.eoApiPrefix === undefined? '/api/v1/':options.eoApiPrefix}${url}`, finalOptions) + .then(async response => { + if (response.status === STATUS_CODE.UNANTHORIZED) { + // 处理401未登录的逻辑,比如跳转到登录页面或弹出登录框 + console.log('Unauthorized access, redirecting to login...'); + window.location.href = '/login' // 示例:重定向到登录 + + return; // 返回或抛出错误,确保不继续执行后续的响应处理 + } + + if (response.status === STATUS_CODE.FORBIDDEN) { + // 处理403无权限,比如跳转到登录页面或弹出登录框 + console.log('Unauthorized access, redirecting to login...'); + // window.location.href = '/login' // 示例:重定向到登录 + + return; // 返回或抛出错误,确保不继续执行后续的响应处理 + } + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + // 如果响应体为JSON且指定了转换键,则转换响应数据 + if ( isJsonHttp(response.headers)) { + const data = await response.json(); + return shouldTransformKeys ? keysToCamel(data,options.eoTransformKeys as string[]) as T:data + } + + return response; + }) + .catch(error => { + // 全局错误处理 + console.error('Global error handler:', error); + throw error; // 可选择重新抛出错误,让组件处理或显示错误信息 + }); + } + return { fetchData } + +} \ No newline at end of file diff --git a/frontend/packages/common/src/hooks/tokens.ts b/frontend/packages/common/src/hooks/tokens.ts new file mode 100644 index 00000000..bdc6d979 --- /dev/null +++ b/frontend/packages/common/src/hooks/tokens.ts @@ -0,0 +1,29 @@ +import { Dispatch, SetStateAction, useEffect, useState } from 'react' + +type BasicInfoType = { + selected_X_apibee_token: string, + tokenId: number | string +} + +const subscriptions: Dispatch>[] = [] + +let tokenState: unknown = { selected_X_apibee_token: '', tokenId: '' } + +const setTokenState = (newState: BasicInfoType | null) => { + if (newState) { + tokenState = { ...tokenState, ...newState } + } else { + tokenState = null + } + subscriptions.forEach((subscription) => { + subscription(tokenState) + }) +} + +export const useTokenBasicInfo = () => { + const [_, newSubscription] = useState(tokenState) + useEffect(() => { + subscriptions.push(newSubscription) + }, []) + return [tokenState, setTokenState] as const +} diff --git a/frontend/packages/common/src/hooks/useInitializeMonaco.ts b/frontend/packages/common/src/hooks/useInitializeMonaco.ts new file mode 100644 index 00000000..10cf2125 --- /dev/null +++ b/frontend/packages/common/src/hooks/useInitializeMonaco.ts @@ -0,0 +1,18 @@ + +import { useEffect,useState } from 'react'; +import { loader } from '@monaco-editor/react'; +import { monaco } from '../monacoConfig'; + +const useInitializeMonaco = () => { + const [initialized, setInitialized] = useState(false); + + useEffect(() => { + if (!initialized) { + loader.config({ monaco }); + loader.init().then(() => { + setInitialized(true); + }); + } + }, [initialized]); +}; +export default useInitializeMonaco; diff --git a/frontend/packages/common/src/hooks/useTest.ts b/frontend/packages/common/src/hooks/useTest.ts new file mode 100644 index 00000000..417f02cf --- /dev/null +++ b/frontend/packages/common/src/hooks/useTest.ts @@ -0,0 +1,271 @@ +// import moment from "moment"; +// +// declare type Timestamp = number +// +// type SafeAny = unknown +// // Header structure +// interface Header { +// key: string +// value: SafeAny +// } +// +// // TimingSummary structure +// interface TimingSummary { +// dnsTiming: string +// tcpTiming: string +// tlsTiming: string +// requestSentTiming: string +// firstByteTiming: string +// contentDeliveryTiming: string +// responseTiming: string +// } +// +// // General structure +// interface General { +// redirectTimes: number +// downloadSize: number +// downloadRate: string +// timingSummary: TimingSummary[] +// time: string +// } +// +// // Request structure +// interface Request { +// headers: Header[] +// body: SafeAny +// requestType: string +// uri?: string +// } +// +// export type ResponseType = 'text' | 'longText' | 'stream' +// export type ResponseContentType = 'formdata' | 'raw' | 'binary' +// +// // Response structure +// interface Response { +// headers: Header[] +// body: SafeAny +// httpCode: number +// testDeny: string +// responseLength: number +// responseType: ResponseType +// contentType: ResponseContentType +// } +// +// // Report structure +// interface Report { +// response: Response +// request: Request +// reportList: Array<{ type: 'throw' | 'interrupt'; content: string }> +// general: General +// blobFileName?: string +// } +// +// // RequestInfo structure +// interface RequestInfo { +// params: SafeAny[] +// apiProtocol: string +// URL: string +// headers: Header[] +// methodType: string +// method: string +// requestType: string +// } +// +// // ResultInfo structure +// interface ResultInfo { +// headers: Header[] +// body: string +// httpCode: number +// testDeny: string +// responseLength: number +// responseType: string +// contentType: string +// reportList: SafeAny[] +// } +// +// // History structure +// interface History { +// afterInject: string +// beforeInject: string +// requestInfo: RequestInfo +// general: General +// resultInfo: ResultInfo +// } +// +// // TestResponse structure +export interface TestResponse { + id: string + report: Report + history: History + globals: SafeAny +} +// +// interface TestInfo { +// createTime: Timestamp +// updateTime: Timestamp +// id: number +// projectUuid: string +// sharedUuid: string +// } +// +// export function useCreateTestHistory() { +// // const { request, loading, error, response, data } = useRequest('/api/api/history', 'POST') +// +// // const { projectId, workspaceId } = useParams() +// +// const createTestHistory = async (data: SafeAny) => { +// const result = await request({ +// projectUuid: projectId, +// workSpaceUuid: workspaceId, +// request: '', +// response: '', +// general: '{}', +// apiUuid: -1, +// ...data +// }) +// return result +// } +// +// return { data: response, raw: data, error, createTestHistory, isLoading: loading } +// } +// +// interface TestRequestProps { +// apiId: string +// projectId: string +// workspaceId: string +// } +// +// interface TestProps { +// uri: string +// method: HTTPMethod +// preScript: string +// postScript: string +// // contentType: SafeAny +// restParams: SafeAny[] +// headersParams: MessageBody[] +// bodyParams: MessageBody[] +// requestType: TestApiBodyType +// authInfo: SafeAny +// } +// +// export function useTest() { +// const { request, loading, error, response, data, cancel } = useRequest('/api/unit', 'POST', { raw: true }) +// const selectedEnv = useEnvStore((state) => state.selectedEnv) as Env +// const { lang, workspaceId, projectId } = useParams() +// const testResponse: TestResponse = (data as { data: TestResponse })?.data +// const language = { en: 'en', zh: 'cn' }[lang] +// const testTime = moment().format('YYYY-MM-DD HH:mm:ss') +// const [globalVariables] = useGlobalVariable() +// +// const { createTestHistory } = useCreateTestHistory() +// +// const format = (props: TestRequestProps, data: TestProps) => { +// const { uri, method, preScript, postScript, requestType, restParams, headersParams, bodyParams, authInfo } = data +// +// const globals = globalVariables || {} +// const headers = +// headersParams?.map( +// (row) => +// ({ +// headerName: row.name, +// headerValue: row.paramAttr.example +// }) || '' +// ) || [] +// +// return { +// action: 'ajax', +// data: { +// lang: language, +// globals, +// URL: formatUri(uri!, restParams), +// method: HTTPMethod[method!], +// methodType: `${method}`, +// httpHeader: 0, // TODO: data.protocol, +// headers: headers, +// requestType: `${requestType}`, +// params: formatBody({ +// requestType, +// data: bodyParams || [] +// }), +// apiRequestParamJsonType: '0', +// advancedSetting: { requestRedirect: 1, checkSSL: 0, sendEoToken: 1, sendNocacheToken: 0 }, +// env: { +// paramList: (selectedEnv?.parameters || []).map((val) => ({ paramKey: val.name, paramValue: val.value })), +// frontURI: selectedEnv?.hostUri +// }, +// auth: { status: '0' }, +// authInfo: authInfo || {}, +// beforeInject: preScript, +// afterInject: postScript, +// testTime +// }, +// id: JSON.stringify({ +// uuid: props.apiId, +// wid: props.workspaceId, +// pid: props.projectId +// }) +// } +// } +// +// const test = async (props: TestRequestProps, data: TestProps) => { +// const testRequest = format(props, data) +// const result: SafeAny = await (request(testRequest)) as unknown as Promise<{ data: TestResponse }> +// await createTestHistory({ +// apiUuid: props.apiId || -1, +// general: '{}', +// request: JSON.stringify({ +// requestParams: { +// headerParams: [], +// bodyParams: [], +// queryParams: [], +// restParams: [] +// }, +// responseList: [], +// uri: data.uri, +// protocol: 0, +// apiAttrInfo: { +// beforeInject: data.preScript, +// afterInject: data.postScript, +// requestMethod: data.method, +// contentType: 1 +// } +// }), +// response: JSON.stringify(result.data.report.response), +// workSpaceUuid: workspaceId +// }) +// // mutate(`getTestHistories?projectUuid=${projectId}&workSpaceUuid=${workspaceId}&page=${1}&pageSize=${200}`) +// return result +// } +// +// return { data: response, raw: testResponse, error, test, format, isLoading: loading, cancel } +// } +// +// function formatBody({ +// requestType, +// data +// }: { +// requestType: ApiBodyType +// data: Partial[] +// }) { +// switch (requestType) { +// case ApiBodyType.Binary: +// case ApiBodyType.Raw: { +// return data?.[0]?.binaryRawData +// } +// case ApiBodyType.FormData: { +// return data?.map((val) => { +// const example = val.paramAttr?.example as string +// const exampleArr = val.paramAttr?.example as FileExample +// const isFile = val.dataType === ApiParamsType.file +// const paramInfo = isFile ? exampleArr?.map((val) => val.name).join(',') : example +// return { +// listDepth: 0, +// paramKey: val.name, +// files: isFile ? exampleArr?.map((file) => file.content) : example, +// paramType: val.dataType === ApiParamsType.file ? '1' : '0', +// paramInfo +// } +// }) +// } +// } +// } diff --git a/frontend/packages/common/src/hooks/webSocket.ts b/frontend/packages/common/src/hooks/webSocket.ts new file mode 100644 index 00000000..7971cb03 --- /dev/null +++ b/frontend/packages/common/src/hooks/webSocket.ts @@ -0,0 +1,47 @@ +import {useState, useCallback} from 'react'; + +type WebSocketHookProps = { + onOpen:()=>void, + onClose:()=>void, + onMessage:(event:unknown)=>void, + onError:(event:unknown)=>void, +} +const useWebSocket = () => { + const [ws, setWs] = useState(); + + // 创建WebSocket连接的方法 + const createWs = (url:string,{onOpen, onClose, onMessage, onError}:WebSocketHookProps) => { + if(ws) { + ws.close(); + } + + const socket = new WebSocket(url); + setWs(socket); + + socket.onopen = () => onOpen && onOpen(); + socket.onclose = () => onClose && onClose(); + socket.onmessage = (event) => onMessage && onMessage(event); + socket.onerror = (error) => onError && onError(error); + + return socket + } + + // 提供发送消息的方法 + const sendMessage = useCallback((message:string) => { + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(message); + } + }, [ws]); + + // 断开连接的方法 + const disconnectWs = useCallback(() => { + if (ws) { + ws.close(); + setWs(null); + } + }, [ws]); + + return { createWs, sendMessage, disconnectWs }; +}; + +export default useWebSocket; \ No newline at end of file diff --git a/frontend/packages/common/src/index.css b/frontend/packages/common/src/index.css new file mode 100644 index 00000000..08d27fe4 --- /dev/null +++ b/frontend/packages/common/src/index.css @@ -0,0 +1,9 @@ +/* + * @Date: 2024-06-04 14:58:33 + * @LastEditors: maggieyyy + * @LastEditTime: 2024-06-04 15:39:24 + * @FilePath: \frontend\packages\common\src\index.css + */ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/frontend/packages/common/src/monacoConfig.ts b/frontend/packages/common/src/monacoConfig.ts new file mode 100644 index 00000000..0bb1cdbe --- /dev/null +++ b/frontend/packages/common/src/monacoConfig.ts @@ -0,0 +1,27 @@ +import { loader } from '@monaco-editor/react'; +import * as monaco from 'monaco-editor'; +import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; +import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; +import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'; +import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'; +import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; + +self.MonacoEnvironment = { + getWorker(_, label) { + if (label === 'json') { + return new jsonWorker(); + } + if (label === 'css' || label === 'scss' || label === 'less') { + return new cssWorker(); + } + if (label === 'html' || label === 'handlebars' || label === 'razor') { + return new htmlWorker(); + } + if (label === 'typescript' || label === 'javascript') { + return new tsWorker(); + } + return new editorWorker(); + }, +}; + +export { monaco }; \ No newline at end of file diff --git a/frontend/packages/common/src/utils/curl.ts b/frontend/packages/common/src/utils/curl.ts new file mode 100644 index 00000000..36310edb --- /dev/null +++ b/frontend/packages/common/src/utils/curl.ts @@ -0,0 +1,431 @@ +/** + * Author: wisenchen + */ +export type ParseCurlResult = { + /* 请求地址 */ + url: string + /* 请求方法 */ + method: string + /* 请求头部字段 */ + headers: { [key: string]: string } + /* 请求 query 参数 */ + query?: { [key: string]: string } + /* 请求内容类型 */ + contentType?: string + /* 请求 body 原文 */ + body?: string + /* 如果是 formData 会解析成对象 */ + requestParams?: { [key: string]: unknown } | string +} +/* parse curl */ + +// Forbidden_header_name list https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name +const disabledFiledList = [ + 'Accept', + 'Accept-Charset', + 'Accept-Encoding', + 'Access-Control-Request-Headers', + 'Access-Control-Request-Method', + 'Accept-Language', + 'Connection', + 'Content-Length', + // 'Cookie', cookie 需要保留 + 'Date', + 'DNT', + 'Expect', + 'Host', + 'Keep-Alive', + 'Origin', + 'Permissions-Policy', + /^Proxy-/i, + /^Sec-/i, + 'Referer', + 'TE', + 'Trailer', + 'Transfer-Encoding', + 'Upgrade', + 'Via', + 'User-Agent' +] + +export class ParseCurl { + private curlStr: string + /* 解析后的对象 */ + private parseObj: ParseCurlResult = { + url: '', + method: '', + query: {}, + headers: {}, + contentType: '', + body: '' + } + + private options = { + /** + * 忽略浏览器中禁止修改的 header + * 参考 https://developer.mozilla.org/en-US/docs/Glossary/Forbidden_header_name + */ + ignoreDisabledHeaders: false + } + /** + * @param curlStr curl 字符串 + * @param options 配置项 + */ + constructor(curlStr: string, options?: { + ignoreDisabledHeaders: boolean; +}) { + this.curlStr = curlStr + this.options = options || this.options + this.validateCurl() + this.parseCurl() + this.ignoreDisabledHeader() + this.resetMethods() + this.parseObj.requestParams = this.getRequestBody2json() + } + + /** 基础校验,验证 curl 字符串的合法性 */ + validateCurl(): string | void { + if (!this.curlStr || typeof this.curlStr !== 'string') { + throw `curl 字符串为空或不是字符串` + } + const trimmedCurl = this.curlStr.trim() + if (!trimmedCurl.toLowerCase().startsWith('curl')) { + throw `不是以 curl 开头的字符串` + } + } + /** + * 解析 curl 字符串后获取到的 body 参数 + */ + getParseBody() { + return this.parseObj.requestParams + } + /** + * 获取请求参数类型 formData json 等 + */ + getBodyType() { + return this.parseObj.contentType + } + /** + * 解析 curl 字符串后获取到的 header 参数 + */ + getParseHeader() { + return this.parseObj.headers + } + + /** + * 获取解析后的完整结果 + */ + getParseResult() { + return this.parseObj + } + + /* 获取解析后的请求 url */ + getParseUrl() { + return this.parseObj.url + } + /* 获取解析后的请求 url */ + getParseMethod() { + return this.parseObj.method + } + /* 获取解析后的请求 query */ + getParseQuery() { + return this.parseObj.query + } + /** + * 重置 methods, 由于像浏览器里中复制的 curl 字符串,可能不会存在 -X 或者 --request 参数,导致无法获取到 method 所以这里只能根据上下文推断 + */ + resetMethods() { + // 如果已经获取到 method 不需要再推断 + if (this.parseObj.method) { + return + } + if (this.parseObj.body) { + // 存在请求参数推断为 post 请求 + this.parseObj.method = 'POST' + return + } + // 默认为 GET + this.parseObj.method = 'GET' + } + parseCurl() { + const result = this.parseObj + // 零宽空格(zero-width space) 需要去除掉 + this.curlStr = this.curlStr.replace(/\u200B/g, '') + const args = rewrite(split(this.curlStr)) + let state = '' + args.forEach((arg: string) => { + switch (true) { + case isURL(arg): + this.parseObj.url = arg + this.parseObj.query = parseUrl2QueryParams(arg) + break + + case arg === '-A' || arg === '--user-agent': + state = 'user-agent' + break + + case arg === '-H' || arg === '--header': + state = 'header' + break + + // 请求体 + case ['-d', '--data', '--data-ascii', '--data-raw', '--data-binary', '--data-urlencode'].includes(arg): + state = 'data' + break + + case arg === '-u' || arg === '--user': + state = 'user' + break + + case arg === '-I' || arg === '--head': + result.method = 'HEAD' + break + + case arg === '-X' || arg === '--request': + state = 'method' + break + + case arg === '-b' || arg === '--cookie': + state = 'cookie' + break + + case arg === '--compressed': + result.headers['Accept-Encoding'] = result.headers['Accept-Encoding'] || 'deflate, gzip' + break + + /** + * State handler + */ + case !!arg: + switch (state) { + case 'header':{ + const field = parseField(arg) + result.headers[field[0]] = field[1] + state = '' + break} + case 'user-agent': + result.headers['User-Agent'] = arg + state = '' + break + case 'data': + if (result.method === 'GET' || result.method === 'HEAD') result.method = 'POST' + + if (!result.headers['content-Type'] && !result.headers['Content-Type']) { + result.headers['content-type'] = + result.headers['Content-Type'] || + result.headers['content-type'] || + 'application/x-www-form-urlencoded' + } + result.body = result.body ? `${result.body}&${arg}` : arg.replace(/^\$/, '') + state = '' + break + case 'user': + result.headers['Authorization'] = `Basic ${btoa(arg)}` + state = '' + break + case 'method': + result.method = arg + state = '' + break + case 'cookie': + result.headers['Set-Cookie'] = arg + state = '' + break + } + break + } + }) + result.headers['Content-Type'] = result.headers['Content-Type'] || result.headers['content-type'] || '' + delete result.headers['content-type'] + result.contentType = result.headers['Content-Type'] + if (result.contentType.includes('multipart/form-data')) { + result.contentType = 'multipart/form-data' + result.headers['Content-Type'] = 'multipart/form-data' + } + this.parseObj = result + } + + getRequestBody2json() { + if (!this.parseObj.body) { + return + } + // const reg = /Content-Disposition: form-data; name=(\^\^.*?\^\^)[\s\S]+?\^([\s\S]+?)\^/g + // const reg = /Content-Disposition: form-data; name=\^\^(.*?)\^\^[\s\S]+?\^\s(\S+?)\^/g // 匹配 cmd curl + /** + * 这个正则需要兼容 cmd 和 bash 两种格式的 multipart/form-data 参数 + */ + + const contentType = this.parseObj.contentType || '' + const body = this.parseObj.body + + if (contentType.includes('multipart/form-data')) { + // 匹配 multipart/form-data 参数的正则表达式 + // 这个正则需要兼容 cmd 和 bash 两种格式的 multipart/form-data 参数 + // const matchMultipartFormDataReg = + // /Content-Disposition: form-data; name=[\^"]*(.*?)[\^";]+[\\r\\n\^\s;]{0,16}(.*?)[\^\s\\n\\r]+(------)?/g + + const requestParams: { [key: string]: unknown } = body + .split(/[\\r\\n\s]*------WebKitForm\w+[\\r\\n\s]*/g) + .filter((str) => str.includes('name="')) + .reduce((prev, curr) => { + const [, key, value] = curr.match(/[\\r\\n\s]*name="([^"]+)";?[\\r\\n\s]*(.*)/) || [] + return Object.assign({}, prev, { + [key]: value.includes('filename=') ? { type: 'file' } : decodeURIComponent(value) + }) + }, {}) + + // 将解析得到的请求参数存入解析对象 + return requestParams + } + + if (contentType.includes('application/x-www-form-urlencoded')) { + // 解析 application/x-www-form-urlencoded 类型的参数 + const formDataParams = decodeURIComponent(body.replace(/\^/g, '')) + return parseUrl2QueryParams(formDataParams, false) + } + + if (contentType.includes('application/json')) { + // 解析 application/json 类型的参数 + return body.replace(/\^\^/g, '"').replace(/\^({|})/g, '$1') + } + + // 默认情况下,直接使用 body 作为请求参数 + // 如果没有匹配到特定的内容类型,将 body 视为请求参数 + return body + } + + /** + * 过滤掉一些不需要的请求头 + */ + ignoreDisabledHeader() { + if (this.options.ignoreDisabledHeaders) { + for (const headerKey in this.parseObj.headers) { + const isDisabledHeader = disabledFiledList.some((key) => + typeof key === 'string' ? headerKey.toLowerCase() === key.toLowerCase() : key.test(headerKey) + ) + if (isDisabledHeader) { + delete this.parseObj.headers[headerKey] + } + } + } + } +} + +/** + * Rewrite args for special cases such as -XPUT. + */ + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function rewrite(args: any[]) { + return args.reduce(function (args, a) { + if (0 === a.indexOf('-X')) { + args.push('-X') + args.push(a.slice(2)) + } else { + args.push(a) + } + + return args + }, []) +} + +/** + * Parse header field. + */ + +function parseField(s: string) { + return s.split(/: (.+)/) +} + +/** + * Check if `s` looks like a url. + */ + +function isURL(s: string) { + try { + const URLObj = new URL(s.trim()) + if (!URLObj.hostname) return false + return true + } catch (e) { + return false + } +} +const parseUrl2QueryParams = (url: string, isCompleteUrl = true) => { + const index = url.indexOf('?') + if (index === -1 && isCompleteUrl) { + return {} + } + const queryStr = url.substring(index + 1) + const queryParamsArr = queryStr.split('&') + const res: { [key: string]: string } = {} + for (const item of queryParamsArr) { + const [key, value] = item.split('=') + res[key] = value + } + return res +} +// https://github.com/jimmycuadra/shellwords/blob/main/src/shellwords.ts +const scan = (string: string, pattern: RegExp, callback: (match: RegExpMatchArray) => void) => { + let result = '' + + while (string.length > 0) { + const match = string.match(pattern) + + if (match && match.index != null && match[0] != null) { + result += string.slice(0, match.index) + result += callback(match) + string = string.slice(match.index + match[0].length) + } else { + result += string + string = '' + } + } + + return result +} +/** + * Splits a string into an array of tokens in the same way the UNIX Bourne shell does. + * https://github.com/jimmycuadra/shellwords/blob/main/src/shellwords.ts + * @param line A string to split. + * @returns An array of the split tokens. + */ +const split = (line = '') => { + const words = [] + let field = '' + scan(line, /\s*(?:([^\s\\'"]+)|'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)"|(\\.?)|(\S))(\s|$)?/, (match) => { + const [, word, sq, dq, escape, garbage, separator] = match + + if (garbage != null) { + throw new Error(`Unmatched quote: ${line}`) + } + + if (word) { + field += word + } else { + let addition + + if (sq) { + addition = sq + } else if (dq) { + addition = dq + } else if (escape) { + addition = escape + } + + if (addition) { + field += addition.replace(/\\(?=.)/, '') + } + } + + if (separator != null) { + words.push(field) + field = '' + } + }) + + if (field) { + words.push(field) + } + + return words +} diff --git a/frontend/packages/common/src/utils/dataTransfer.ts b/frontend/packages/common/src/utils/dataTransfer.ts new file mode 100644 index 00000000..d5be17f7 --- /dev/null +++ b/frontend/packages/common/src/utils/dataTransfer.ts @@ -0,0 +1,25 @@ + +import { ColumnFilterItem } from 'antd/es/table/interface' +import {DepartmentListItem} from '@core/const/member/type' +import { RcFile } from 'antd/es/upload'; + +export const handleDepartmentListToFilter:(departmentList:DepartmentListItem[])=>ColumnFilterItem[] = (departmentList:DepartmentListItem[])=>{ + return departmentList?.map((x:DepartmentListItem)=>( + { + text:x.name, + value:x.id, + children:x.children ? handleDepartmentListToFilter(x.children):null + } + )) +} + +export const getImgBase64 = (img: RcFile, callback: (url: string) => void) => { + const reader = new FileReader(); + reader.addEventListener('load', () => callback(reader.result as string)); + reader.readAsDataURL(img); +}; + + +export const frontendTimeSorter = (a:{[k:string]:string},b: { [k: string]: string }, field:string) =>{ + return (new Date((a)[field])).getTime() - (new Date((b)[field])).getTime() +} \ No newline at end of file diff --git a/frontend/packages/common/src/utils/download.ts b/frontend/packages/common/src/utils/download.ts new file mode 100644 index 00000000..ee95c1a6 --- /dev/null +++ b/frontend/packages/common/src/utils/download.ts @@ -0,0 +1,78 @@ +export const decodeBase64ToUnicode = (base64Str: string): string => { + // Decode base64 to a string using atob and convert it to a unicode string. + return decodeURIComponent( + atob(base64Str) + .split('') + ?.map((char) => `%${char.charCodeAt(0).toString(16).padStart(2, '0')}`) + .join('') + ) +} + +interface DownloadOptions { + body: string + contentType: 'formdata' | 'raw' | 'binary' + responseType: 'text' | 'longText' | 'stream' + filename: string + uri: string +} + +const stringToArrayBuffer = (str: string): ArrayBuffer => { + const buffer = new ArrayBuffer(str.length) + const view = new Uint8Array(buffer) + for (let i = 0; i < str.length; i++) { + view[i] = str.charCodeAt(i) & 0xff + } + return buffer +} + +export const createBlobUrl = (data: string, fileType: string): string => { + // Create a Blob URL from the given data and file type. + const blob = new Blob([stringToArrayBuffer(data)], { type: fileType }) + return URL.createObjectURL(blob) +} + +export const downloadFile = ({ body, contentType, responseType, filename, uri }: DownloadOptions): void => { + let content = body + + // Decode the body if needed. + if (responseType === 'longText' || responseType === 'stream') { + content = decodeBase64ToUnicode(content) + } else if (responseType === 'text') { + try { + content = JSON.stringify(JSON.parse(content), null, 4) + } catch { + // Fallback to raw content on parsing error. + } + } + + // Create a Blob URL for downloading. + const blobUrl = contentType.startsWith('image') ? uri : createBlobUrl(content, contentType) + const downloadFilename = decodeURI(filename || 'default_name') + + // Create a temporary anchor element for downloading. + const anchor = document.createElement('a') + anchor.style.display = 'none' + anchor.href = blobUrl + anchor.download = downloadFilename + document.body.appendChild(anchor) + anchor.click() + document.body.removeChild(anchor) + + // Revoke the Blob URL to free resources. + URL.revokeObjectURL(blobUrl) +} + +export const downloadFileFromText = (fileName: string, text: string) => { + const file = new Blob([text], { type: 'data:text/plain;charset=utf-8' }) + + const element = document.createElement('a') + const url = URL.createObjectURL(file) + element.href = url + element.download = fileName + document.body.appendChild(element) + element.click() + Promise.resolve().then(() => { + document.body.removeChild(element) + window.URL.revokeObjectURL(url) + }) +} diff --git a/frontend/packages/common/src/utils/navigation.tsx b/frontend/packages/common/src/utils/navigation.tsx new file mode 100644 index 00000000..3b96d42d --- /dev/null +++ b/frontend/packages/common/src/utils/navigation.tsx @@ -0,0 +1,54 @@ + +import { MenuItem } from "@common/components/aoplatform/Navigation"; + +export function getNavItem( + label: React.ReactNode, + key: React.Key, + path:string, + icon?: React.ReactNode, + children?: MenuItem[], + type?: 'group', + access?:string[] | string +): MenuItem { + return { + key, + icon :icon , + path, + routes:children, + name:label, + type, + access + } as MenuItem; +} + +export function getItem( + label: React.ReactNode, + key: React.Key, + icon?: React.ReactNode, + children?: MenuItem[], + type?: 'group', + access?:string[] | string + ): MenuItem { + return { + key, + icon, + children, + label, + type, + access + } as MenuItem; + } + + export function getTabItem( + label: React.ReactNode, + key: React.Key, + children?: MenuItem[], + type?: 'group', + access?:string + ) { + return { + key, + label, + access + } + } \ No newline at end of file diff --git a/frontend/packages/common/src/utils/permission.ts b/frontend/packages/common/src/utils/permission.ts new file mode 100644 index 00000000..8c6bc5fe --- /dev/null +++ b/frontend/packages/common/src/utils/permission.ts @@ -0,0 +1,28 @@ + +import { PERMISSION_DEFINITION } from "@common/const/permissions" +import { AccessDataType } from "@common/const/type" + + +export const checkAccess:(access:AccessDataType, accessData:Map)=>boolean = (access, accessData)=>{ + if(!access){ + return true + } + const accLevel = access.split('.')[0] + if(['system','team'].indexOf(accLevel) === -1){ + console.warn('权限字段有误:',access) + return false + } + const neededBackendAccessArr = PERMISSION_DEFINITION[0]?.[access]?.granted.anyOf[0].backend || [] + return accessData?.has(accLevel)&& accessData.get(accLevel)!.length > 0 ? hasIntersection(neededBackendAccessArr, accessData.get(accLevel)!) : false +} + +const hasIntersection = (arr1:string[], arr2:string[])=> { + const set = new Set(arr1.length > arr2.length ? arr2:arr1) + const arr = arr1.length > arr2.length ? arr1:arr2 + for (const item of arr) { + if (set.has(item)) { + return true; // 发现交集 + } + } + return false; // 没有交集 + } \ No newline at end of file diff --git a/frontend/packages/common/src/utils/postcat.tsx b/frontend/packages/common/src/utils/postcat.tsx new file mode 100644 index 00000000..9e438723 --- /dev/null +++ b/frontend/packages/common/src/utils/postcat.tsx @@ -0,0 +1,269 @@ +import {ReactNode} from "react"; +import {ApiBodyType} from "../const/api-detail"; +import {ContentType} from "@common/components/postcat/api/ApiTest/components/ApiRequestTester/TestBody/const.ts"; + +declare type CheckedStatus = 'checked' | 'unchecked' | 'indeterminate' + +export interface TreeNode> { + id: string + children?: T[] + path?: string[] + parent?: T | null + __raw__?: T + __globalIndex__?: number + __levelIndex__?: number + __hasSiblingLeaf__?: boolean +} + + +interface CommonTreeNode> { + children?: T[] + childList?: T[] + parent?: T +} +/** + * Flattens a hierarchical tree structure into a flat array of nodes, + * each enhanced with a path property representing its location within the tree. + */ +export function flattenTree>( + tree: T[] = [], + childrenKey: keyof T = 'children', + pathKey: keyof T = 'id' +): T[] { + const result: T[] = [] + let __globalIndex__ = 0 + + const flatten = (node: T, path: string[], __levelIndex__: number, parent: T | null = null): void => { + const { [childrenKey]: children, ...restNode } = node + const nodeWithPath: T = { + ...restNode, + __raw__: node, + path: [...path, node[pathKey]] as string[], + __globalIndex__, + __levelIndex__ + } as T + nodeWithPath[childrenKey] = children + nodeWithPath.parent = parent ?? null + result.push(nodeWithPath) + __globalIndex__++ + + const list: T[] = (children || []) as unknown as T[] + list.forEach((child: T, childIndex: number) => flatten(child, nodeWithPath.path || [], childIndex, node)) + } + + tree.forEach((node, index) => flatten(node, [], index)) + return result +} + + + +export function byteToString(inputByteLength: number): string { + inputByteLength = inputByteLength || 0 + + // Define thresholds for byte units + const KB = 1024 + const MB = 1024 * KB + const GB = 1024 * MB + + // Helper function to format the byte length into a string + const formatSize = (size: number, unit: string) => { + const formattedSize = size.toFixed(2) + // Remove unnecessary '.00' + if (formattedSize.endsWith('.00')) { + return `${parseInt(formattedSize, 10)} ${unit}` + } + return `${formattedSize} ${unit}` + } + + // Convert and format byte length to appropriate unit + if (inputByteLength < 0.1 * KB) { + return formatSize(inputByteLength, 'B') + } else if (inputByteLength < 0.1 * MB) { + return formatSize(inputByteLength / KB, 'KB') + } else if (inputByteLength < 0.1 * GB) { + return formatSize(inputByteLength / MB, 'MB') + } else { + return formatSize(inputByteLength / GB, 'GB') + } +} + +export function determineCheckState(items: T[]): CheckedStatus { + let allChecked = true + let allUnchecked = true + + for (const item of items) { + if (item.isRequired) { + allUnchecked = false + } else { + allChecked = false + } + + if (!allChecked && !allUnchecked) { + return 'indeterminate' + } + } + + return allChecked ? 'checked' : allUnchecked ? 'unchecked' : 'indeterminate' +} + +export function generateId(): string { + return Math.random().toString(36).slice(-8) +} + + +export const getActionColWidth = (actionButtonCount: number) => { + if (actionButtonCount === 0) return 50 + return actionButtonCount * 30 + 20 +} + +export function renderComponent(content: ReactNode | null | undefined, fallbackComponent: ReactNode): ReactNode | null { + if (content === null) return null + return content ?? fallbackComponent +} + +export function isNil(value: unknown): value is null | undefined { + return typeof value === 'undefined' || value === null +} + +export function traverse>( + node: T | T[] | null, + cb: (node: T, level: number) => void, + childrenKey: keyof T = 'children' +): void { + if (!node) return; + const queue: { node: T; level: number }[] = Array.isArray(node) ? node.map(n => ({ node: n, level: 0 })) : [{ node, level: 0 }]; + while (queue.length) { + const { node: currentNode, level } = queue.shift()! + cb(currentNode, level); + const children = currentNode[childrenKey] as T[] | undefined; + if (children && children.length > 0) { + queue.push(...children.map(child => ({ node: child, level: level + 1 }))); + } + } +} + +export function generateNumberId(digit: number = 15): number { + let result = '' + for (let i = 0; i < digit; i++) { + result += Math.floor(Math.random() * 10).toString() + } + return +result +} + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + type SafeAny = any +export const getQueryFromURL = (url: string): { [key: string]: string } => { + const result: SafeAny = {}; + //? prevent double question mark + new URLSearchParams(url.split('?').slice(1).join('?')).forEach((val, name) => { + result[name] = val; + }); + return result; +}; + +/** + * Sync URL and Query + * + * @description Add query to URL and read query form url + * @param url - whole url include query + * @param query - ui query param + * @param opts.method - sync method + * @returns - {url:"",query:[]} + */ +export const syncUrlAndQuery = ( + url = '', + query = [], + opts: { + nowOperate?: 'url' | 'query'; + method: 'replace' | 'keepBoth'; + } = { + method: 'replace', + nowOperate: 'url' + } +) => { + const urlQuery: SafeAny[] = []; + const uiQuery = query; + //Get url query + const queryObj = getQueryFromURL(url); + Object.keys(queryObj).forEach(name => { + const value = queryObj[name]; + const item: SafeAny = { + isRequired: 1, + name, + paramAttr: { + example: value + } + }; + urlQuery.push(item); + }); + const pre = opts.nowOperate === 'url' ? uiQuery : urlQuery; + const next = opts.nowOperate === 'url' ? urlQuery : uiQuery; + const result: SafeAny = { + url, + query + }; + if (opts.method === 'replace') { + result.query = [...next, ...pre.filter(val => !val.isRequired)]; + } else { + result.query = [ + ...next.map(val => Object.assign(pre.find(val1 => val1.name === val.name) || {}, val)), + ...pre.filter((val: SafeAny) => urlQuery.every(val1 => val1.name !== val.name)) + ]; + } + result.url = jointQuery(url, result.query); + return result; +}; + +const jointQuery = (url = '', query: SafeAny[]) => { + //Joint query + let search = ''; + query.forEach(val => { + if (!(val.name && val.isRequired)) { + return; + } + search += `${val.name}=${val.paramAttr?.example || ''}&`; + }); + search = search ? `?${search.slice(0, -1)}` : ''; + return `${url.split('?')[0]}${search}`; +}; + + +export function extractBraceContent(uri: string): string[] | null { + // Regular expression to match content inside curly braces + const regex = /{([^}]+)}/g + let match: RegExpExecArray | null + const results: string[] = [] + + // Loop to find all matches + while ((match = regex.exec(uri)) !== null) { + // Add the matched content to the results array + results.push(match[1]) + } + + // Return null if no matches were found + return results.length > 0 ? results : null +} + + +export function mapContentTypeToApiBodyType(type: ContentType): ApiBodyType { + const contentType = + { + 'text/plain': ApiBodyType.Raw, + 'application/json': ApiBodyType.JSON, + 'application/xml': ApiBodyType.XML, + 'text/html': ApiBodyType.XML, + 'application/javascript': ApiBodyType.Raw, + 'application/x-www-form-urlencoded': ApiBodyType.FormData, + 'multipart/form-data': ApiBodyType.FormData + }[type ?? 'text/plain'] ?? ApiBodyType.Raw + return contentType +} + +export function file2Base64(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.readAsDataURL(file) + reader.onload = (): void => resolve(reader.result as string) + reader.onerror = (error): void => reject(error) + }) +} \ No newline at end of file diff --git a/frontend/packages/common/src/utils/router.ts b/frontend/packages/common/src/utils/router.ts new file mode 100644 index 00000000..92bf9676 --- /dev/null +++ b/frontend/packages/common/src/utils/router.ts @@ -0,0 +1,24 @@ +export const objectToSearchParameters = (obj:Record, prefix?:string)=>{ + const params = new URLSearchParams(); + + for (const key in obj) { + const value = obj[key]; + const prefixedKey = prefix ? `${prefix}[${key}]` : key; + + if(value === undefined) continue + if (Array.isArray(value)) { + // 如果值是数组,展开数组每个元素为单独的键值对 + value.forEach((item, index) => { + params.append(`${prefixedKey}[${index}]`, item); + }); + } else if (value !== null && typeof value === 'object') { + // 如果值是对象,递归处理 + params.append(prefixedKey, JSON.stringify(value)); // 将嵌套对象转换为字符串 + } else { + // 否则,直接添加键值对 + params.append(prefixedKey, value); + } + } + + return params; +} \ No newline at end of file diff --git a/frontend/packages/common/src/utils/systemRunning.ts b/frontend/packages/common/src/utils/systemRunning.ts new file mode 100644 index 00000000..6c71a68f --- /dev/null +++ b/frontend/packages/common/src/utils/systemRunning.ts @@ -0,0 +1,299 @@ + + /** + * @description 获取全局应用关系视图(即空间下所有项目组的 api 关联关系) + * @param request + * @returns + */ + // export function getSpaceProjectGroupRelative(request: GetSpaceProjectGroupRelativeRequest) { + // return this.http.post( + // '/javaApi/topology/project/get-all-relations', + // request, + // { + // headers: { + // 'content-type': 'application/json' + // } + // } + // ) + // } + +import G6, { EdgeConfig } from "@antv/g6" +import { SELF_SPACE_CONTENT_COLOR, OUT_SPACE_CONTENT_COLOR, RELATIVE_PICTURE_NODE_FONTSIZE, SELF_SPACE_THEME, OUT_SPACE_THEME } from "@core/const/system-running/const" +import { NodeData } from "@core/const/system-running/type" +import { TopologyProjectItem, TopologyServiceItem } from "@core/pages/systemRunning/SystemRunning" + + /** + * @description 获取全局应用关系视图(即空间下所有项目组的 api 关联关系) + * @param request + * @returns + */ + // export function getProjectGroupRelativeData(request: ProjectGroupRelativeRequest) { + // return this.http.post('/javaApi/topology/project/get-focus-relations', request, { + // headers: { + // 'content-type': 'application/json' + // } + // }) + // } + + + + export const nodesFormatter:(nodes:TopologyProjectItem[], isSelfSpace?:boolean)=> NodeData[] = (nodes,isSelfSpace=true) =>{ + return nodes + .filter((item) => item) + .map((item) => { + if(isSelfSpace === undefined) {isSelfSpace = true} + const theme = isSelfSpace ? SELF_SPACE_CONTENT_COLOR : OUT_SPACE_CONTENT_COLOR + const name = `${item.name}` + const nodeData: NodeData = { + id: item.id, + label: fittingString(name, 150, RELATIVE_PICTURE_NODE_FONTSIZE), + name: name, + isSelfSpace, + isApp:item.isApp, + isServer:item.isServer, + x: 250, + y: 150, + title: name, + style: { + // 自身空间内的边框颜色和其他空间的边框颜色不一致 + stroke: theme, + border: theme, + fill: isSelfSpace ? SELF_SPACE_THEME : OUT_SPACE_THEME + }, + stateStyles: { + selected: { + // 选中状态的颜色 + fill: theme + } + } + } + return nodeData + }) +} + /** + * @description + */ + export function edgesFormatter(projectConnectMap:Map>) { + const edges: EdgeConfig[] = []; + for (const [projectKey, invokedMap] of projectConnectMap) { + for (const [invokedProjectId] of invokedMap) { // 这里使用了 for...of 遍历 Map + edges.push({ + source: projectKey, + target: invokedProjectId, + _projectInfo: invokedMap.get(invokedProjectId), + }); + } + } + return edges; + + } + + /** + * @description 修正节点,过长省略 + * @param str + * @param maxWidth + * @param fontSize + * @returns + */ + export const fittingString = (str: string, maxWidth: number, fontSize: number) => { + const ellipsis = '...' + const ellipsisLength = G6.Util.getTextSize(ellipsis, fontSize)[0] + let currentWidth = 0 + let res = str + const pattern = new RegExp('[\u4E00-\u9FA5]+') + str.split('').forEach((letter, i) => { + if (currentWidth > maxWidth - ellipsisLength) return + if (pattern.test(letter)) { + currentWidth += fontSize + } else { + currentWidth += G6.Util.getLetterWidth(letter, fontSize) + } + if (currentWidth > maxWidth - ellipsisLength) { + res = `${str.substr(0, i)}${ellipsis}` + } + }) + return res + } + + + /** + * 动态获取节点距离 + */ + export const getNodeSpacing= (num: number,nodes?:unknown) => { + if(nodes) { + const base:number = getNodeSpacing(num) as number + return (d:{comboId:string})=>{ + return d.comboId === 'none-combo' ? (base /2):base + } + } + if (num <= 15) { + return 100 + } + let result = 100 + let base = 15 + while (num > base) { + result = Math.ceil(result / 2) + base *= 15 + } + return result + } + +// const registerEdge = () => { +// G6.registerEdge( +// 'line-running', +// { +// options: { +// style: EDGE_STYLE +// }, + +// afterDraw: (cfg, group) => { +// const lineDash = [4, 2, 1, 2] +// if (!group) return +// const shape = group.get('children')[0] +// let index = 0 +// // Define the animation +// shape.animate( +// () => { +// index = index + 0.4 +// if (index > 1000) { +// index = 0 +// } +// const res = { +// lineDash, +// lineDashOffset: -index +// } +// return res +// }, +// { +// repeat: true, +// duration: 5000 +// } +// ) +// }, + +// setState: (name, value, item) => { +// if (!item || !name) return +// const shape = item.get('keyShape') +// const itemStatus = item.getStates() + +// if ( +// !['edge-success', 'edge-error', 'edge-transparent'].includes(name) && +// itemStatus.some((state) => ['edge-error', 'edge-success', 'edge-transparent'].includes(state)) +// ) +// return +// const theme = item?._cfg?.model?.style?.stroke || SELF_SPACE_THEME +// if (name === 'running') { +// if (value) { +// shape.attr({ +// lineWidth: 4, +// shadowColor: theme, +// shadowBlur: 2 +// }) +// } else { +// shape.attr(EDGE_STYLE) +// } +// } +// } +// }, +// 'quadratic' +// ) +// } + +// private initGraph = (container: ElementRef) => { +// const element = container.nativeElement +// this.graph = new G6.Graph({ +// container: container.nativeElement, +// plugins: [this.tooltip], +// layout: { +// type: 'force', +// // 稳定系数,初始动画的加载时长(稳定性)=节点数量/稳定系数 +// alphaDecay: 0.08, +// // 因为有分组的存在,整体布局需要往左偏移一点 +// center: [element.scrollWidth / 2 - 150, element.scrollHeight / 2], +// preventOverlap: true, +// linkDistance: (d: nodeAny) => { +// if (d.source.id === 'node0') { +// return 100 +// } +// return 30 +// }, +// nodeStrength: (d: nodeAny) => { +// if (d.isLeaf) { +// return -50 +// } +// return -10 +// }, +// edgeStrength: (d: nodeAny) => { +// if (d.source.id === 'node1' || d.source.id === 'node2' || d.source.id === 'node3') { +// return 0.7 +// } +// return 0.1 +// } +// }, +// modes: { +// default: ['drag-canvas', 'drag-node', 'zoom-canvas'] +// }, +// defaultNode: { +// size: [24, 24], +// style: { +// radius: 5, +// stroke: '#69c0ff', +// lineWidth: 1, +// fillOpacity: 1 +// }, +// labelCfg: { +// style: { +// fontSize: RELATIVE_PICTURE_NODE_FONTSIZE, +// fill: this.textColor +// }, +// position: 'bottom', +// offset: 12 +// } +// }, +// defaultEdge: { +// type: 'line-running', +// label: '详情', +// labelCfg: { +// style: { +// fill: '5B8FF9', +// opacity: 0 // 将透明度设置为0,隐藏提示信息,hover 才出现 +// } +// } +// } +// }) +// } + +export class UnionFind { + private parent: Record; + private rank: Record; + + constructor(initialNodes: string[]) { + this.parent = {}; + this.rank = {}; + initialNodes.forEach((node) => { + this.parent[node] = node; + this.rank[node] = 0; + }); + } + + find(node: string): string { + if (node !== this.parent[node]) { + this.parent[node] = this.find(this.parent[node]); + } + return this.parent[node]; + } + + union(node1: string, node2: string): void { + const root1 = this.find(node1); + const root2 = this.find(node2); + if (root1 !== root2) { + if (this.rank[root1] < this.rank[root2]) { + this.parent[root1] = root2; + } else if (this.rank[root1] > this.rank[root2]) { + this.parent[root2] = root1; + } else { + this.parent[root2] = root1; + this.rank[root1]++; + } + } + } +} \ No newline at end of file diff --git a/frontend/packages/common/src/utils/uploadPic.ts b/frontend/packages/common/src/utils/uploadPic.ts new file mode 100644 index 00000000..4d9b00f5 --- /dev/null +++ b/frontend/packages/common/src/utils/uploadPic.ts @@ -0,0 +1,55 @@ +import { RcFile } from "antd/es/upload"; + +export const normFile = (e: unknown) => { + if (Array.isArray(e)) { + return e; + } + return( e as {fileList:unknown} )?.fileList; + }; + + + export const compressImage = (file: RcFile, maxSize: number): Promise => { + const img = document.createElement('img'); + const canvas = document.createElement('canvas'); + const reader = new FileReader(); + + return new Promise((resolve, reject) => { + reader.onload = (e) => { + img.src = e.target?.result as string; + img.onload = () => { + let quality = 0.9; + let width = img.width; + let height = img.height; + + const ctx = canvas.getContext('2d'); + + const compress = () => { + canvas.width = width; + canvas.height = height; + ctx?.clearRect(0, 0, width, height); + ctx?.drawImage(img, 0, 0, width, height); + + const dataUrl = canvas.toDataURL(file.type, quality); + const base64 = dataUrl.split(',')[1]; + return { base64, size: base64.length * 0.75 }; + }; + + let { base64, size } = compress(); + + while (size > maxSize && quality > 0.1) { + quality -= 0.1; + ({ base64, size } = compress()); + } + + while (size > maxSize && (width > 50 || height > 50)) { + width *= 0.9; + height *= 0.9; + ({ base64, size } = compress()); + } + resolve(base64); + }; + }; + reader.onerror = (e) => reject(e); + reader.readAsDataURL(file); + }); + }; \ No newline at end of file diff --git a/frontend/packages/common/src/utils/ux.ts b/frontend/packages/common/src/utils/ux.ts new file mode 100644 index 00000000..3f5b8d94 --- /dev/null +++ b/frontend/packages/common/src/utils/ux.ts @@ -0,0 +1,12 @@ + +export const withMinimumDelay = (fn: () => Promise, delay: number = 100): Promise => { + const startTime = Date.now(); + return fn().then(async result => { + const endTime = Date.now(); + const elapsed = endTime - startTime; + if (elapsed < delay) { + await new Promise(resolve => setTimeout(resolve, delay - elapsed)); + } + return result; + }); + }; \ No newline at end of file diff --git a/frontend/packages/common/src/utils/validate.ts b/frontend/packages/common/src/utils/validate.ts new file mode 100644 index 00000000..9e948ad2 --- /dev/null +++ b/frontend/packages/common/src/utils/validate.ts @@ -0,0 +1,7 @@ + +export const validateUrlSlash = (_, value) => { + if (value && value.includes('//')) { + return Promise.reject(new Error('暂不支持带有双斜杠//的url')); + } + return Promise.resolve(); + }; \ No newline at end of file diff --git a/frontend/packages/common/tailwind.config.js b/frontend/packages/common/tailwind.config.js new file mode 100644 index 00000000..1f606491 --- /dev/null +++ b/frontend/packages/common/tailwind.config.js @@ -0,0 +1,96 @@ +/* + * @Date: 2024-06-04 15:05:05 + * @LastEditors: maggieyyy + * @LastEditTime: 2024-08-01 17:59:56 + * @FilePath: \frontend\packages\common\tailwind.config.js + */ +/** @type {import('tailwindcss').Config} */ +module.exports = { + important:true, + content: [ + `../*/src/**/*.{js,ts,jsx,tsx}`, + ] + , + theme: { + extend: { + width: { + INPUT_NORMAL: '100%', + // INPUT_NORMAL: '346px', + INPUT_LARGE: '508px', + GROUP: '240px', + SEARCH: '276px', + LOG: '254px' + }, + minHeight:{ + TEXTAREA:'68px' + }, + borderRadius: { + DEFAULT: 'var(--border-radius)', + SEARCH_RADIUS: '50px' + }, + boxShadow:{ + SCROLL: '0 2px 2px #0000000d', + SCROLL_TOP:' 0 -2px 2px -2px var(--border-color)' + }, + colors: { + DISABLE_BG: 'var(--disabled-background-color)', + MAIN_TEXT: 'var(--text-color)', + MAIN_HOVER_TEXT: 'var(--text-hover-color)', + SECOND_TEXT:'var(--disabled-text-color)', + MAIN_BG: 'var(--background-color)', + MENU_BG:'var(--MENU-BG-COLOR)', + 'bar-theme': 'var(--bar-background-color)', + BORDER: 'var(--border-color)', + NAVBAR_BTN_BG: 'var(--item-active-background-color)', + MAIN_DISABLED_BG: 'var(--disabled-background-color)', + theme: 'var(--primary-color)', + DESC_TEXT: 'var(--TITLE_TEXT)', + HOVER_BG: 'var(--item-hover-background-color)', + guide_cluster: '#ee6760', + guide_upstream: '#f9a429', + guide_api: '#71d24d', + guide_publishApi: '#5884ff', + guide_final: '#915bf9', + table_text: 'var(--table-text-color)', + status_success:'#138913', + status_fail:"#ff3b30", + status_update:"#03a9f4", + status_pending:"#ffa500", + status_offline:"#8f8e93", + A_HOVER:'var(--button-primary-hover-background-color)' + }, + backgroundImage:{ + LAYOUT_BG:'linear-gradient(107.97deg, rgba(32,41,117,1) 4.41%,rgba(16,13,27,1) 86.11%)', + LAYOUT_BG_DARK:'#fff', + }, + spacing: { + mbase: 'var(--FORM_SPAN)', + label: '12px', // 选择器和label之间的间距,待删 + btnbase: 'var(--LAYOUT_MARGIN)', // x方向的间距 + btnybase: 'var(--LAYOUT_MARGIN)', // y轴方向的间距 + btnrbase: '20px', // 页面最右侧边距20px + formtop: 'var(--FORM_SPAN)', + icon: '5px', + blockbase: '40px', + DEFAULT_BORDER_RADIUS: 'var(--border-radius)', + TREE_TITLE:'var(--small-padding) var(--LAYOUT_PADDING);', + 'navbar-height': 'var(--layout-header-height)', + }, + borderColor: { + 'color-base': 'var(--border-color)' + } + } + }, + plugins: [ + function({ addUtilities }) { + addUtilities({ + '.h-calc-100vh-minus-navbar': { + height: 'calc(100vh - var(--layout-header-height))', + }, + }, ['responsive', 'hover']); + } + ], + corePlugins: { + preflight: false, + }, +} \ No newline at end of file diff --git a/frontend/packages/common/tsconfig.json b/frontend/packages/common/tsconfig.json new file mode 100644 index 00000000..c121206d --- /dev/null +++ b/frontend/packages/common/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + /* Linting */ + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "paths": { + "@common/*": ["./src/*"], + "@core/*": ["../core/src/*"], + "@market/*": ["../market/src/*"] + }, + }, + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/packages/common/tsconfig.node.json b/frontend/packages/common/tsconfig.node.json new file mode 100644 index 00000000..42872c59 --- /dev/null +++ b/frontend/packages/common/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/packages/common/vite.config.ts b/frontend/packages/common/vite.config.ts new file mode 100644 index 00000000..d669dfc6 --- /dev/null +++ b/frontend/packages/common/vite.config.ts @@ -0,0 +1,48 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' +import dynamicImportVars from '@rollup/plugin-dynamic-import-vars'; + +export default defineConfig({ + css: { + preprocessorOptions: { + less: { + javascriptEnabled: true, + }, + }, + modules:{ + localsConvention:"camelCase", + generateScopedName:"[local]_[hash:base64:2]" + } + }, + plugins: [react(), + dynamicImportVars({ + include:["src"], + exclude:[], + warnOnError:false + }), + ], + resolve: { + alias: [ + { find: /^~/, replacement: '' }, + { find: '@common', replacement: path.resolve(__dirname, './src') }, + { find: '@market', replacement: path.resolve(__dirname, '/./market/src') }, + { find: '@core', replacement: path.resolve(__dirname, '../core/src') }, + ] + }, + server: { + proxy: { + '/api/v1': { + // target: 'http://uat.apikit.com:11204/mockApi/aoplatform/', + target: 'http://172.18.166.219:8288/', + changeOrigin: true, + }, + '/api2/v1': { + // target: 'http://uat.apikit.com:11204/mockApi/aoplatform/', + target: 'http://172.18.166.219:8288/', + changeOrigin: true, + } + } + }, + logLevel:'info' +}) diff --git a/frontend/packages/core/.env b/frontend/packages/core/.env new file mode 100644 index 00000000..de1f4bed --- /dev/null +++ b/frontend/packages/core/.env @@ -0,0 +1 @@ +VITE_APP_MODE=openSource \ No newline at end of file diff --git a/frontend/packages/core/.eslintrc.cjs b/frontend/packages/core/.eslintrc.cjs new file mode 100644 index 00000000..87e6dac6 --- /dev/null +++ b/frontend/packages/core/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs','public','code-snippet','ace-editor'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/frontend/packages/core/.gitignore b/frontend/packages/core/.gitignore new file mode 100644 index 00000000..c5647721 --- /dev/null +++ b/frontend/packages/core/.gitignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local +public/tinymce + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + diff --git a/frontend/packages/core/README.md b/frontend/packages/core/README.md new file mode 100644 index 00000000..0d6babed --- /dev/null +++ b/frontend/packages/core/README.md @@ -0,0 +1,30 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +} +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/frontend/packages/core/__tests__/hooks/copy.test.ts b/frontend/packages/core/__tests__/hooks/copy.test.ts new file mode 100644 index 00000000..92b3d19f --- /dev/null +++ b/frontend/packages/core/__tests__/hooks/copy.test.ts @@ -0,0 +1,103 @@ + +import { renderHook, act } from '@testing-library/react-hooks'; +import useCopyToClipboard from '../../src/hooks/copy'; + +describe('useCopyToClipboard', () => { + let navigatorClipboardSpy; + let execCommandSpy; + let isSecureContextOriginalValue; + + beforeEach(() => { + isSecureContextOriginalValue = window.isSecureContext; + window.isSecureContext = true; + // 确保 navigator 对象存在 + if (!globalThis.navigator) { + // @ts-expect-error navigator object may not exist in some environments + globalThis.navigator = {}; + } + // 确保 clipboard 对象存在 + console.log(globalThis.navigator, navigator,navigator.clipboard) + if (!navigator.clipboard) { + // @ts-expect-error clipboard object may not exist in some environments + navigator.clipboard = {}; + navigator.clipboard.writeText = jest.fn().mockImplementation(()=>{ + return Promise.resolve('') + }); + } + // 使用 jest.fn() 创建 writeText 方法的模拟函数 + navigatorClipboardSpy = jest.spyOn(navigator.clipboard, 'writeText'); + // navigatorClipboardSpy.mockReset(); + document.execCommand = jest.fn().mockImplementation(() => true); + execCommandSpy = jest.spyOn(document, 'execCommand'); + }); + + afterEach(() => { + window.isSecureContext = isSecureContextOriginalValue; + }); + + it('should define copyToClipboard function', () => { + const { result } = renderHook(() => useCopyToClipboard()); + expect(result.current.copyToClipboard).toBeDefined(); + }); + + it('should initialize isCopied as false', () => { + const { result } = renderHook(() => useCopyToClipboard()); + expect(result.current.isCopied).toBe(false); + }); + + it('should set isCopied back to false after 3 seconds', async () => { + jest.useFakeTimers(); + const { result } = renderHook(() => useCopyToClipboard()); + navigatorClipboardSpy.mockResolvedValueOnce(); + await act(async () => result.current.copyToClipboard('test')); + jest.advanceTimersByTime(3000); + expect(result.current.isCopied).toBe(false); + jest.useRealTimers(); + }); + + it('should call navigator.clipboard.writeText when copyToClipboard is called', async () => { + const { result } = renderHook(() => useCopyToClipboard()); + await act(async () => result.current.copyToClipboard('test')); + expect(navigatorClipboardSpy).toHaveBeenCalledWith('test'); + }); + it('should call document.execCommand when navigator.clipboard is not available', async () => { + window.isSecureContext = false; + const { result } = renderHook(() => useCopyToClipboard()); + await act(async () => result.current.copyToClipboard('test')); + expect(execCommandSpy).toHaveBeenCalledWith('copy'); + }); + + it('should set isCopied to true when document.execCommand is successful', async () => { + window.isSecureContext = false; + const { result } = renderHook(() => useCopyToClipboard()); + await act(async () => result.current.copyToClipboard('test')); + expect(result.current.isCopied).toBe(true); + }); + + it('should keep isCopied as false when document.execCommand fails', async () => { + window.isSecureContext = false; + document.execCommand = jest.fn().mockImplementation(() => false); + const { result } = renderHook(() => useCopyToClipboard()); + await act(async () => result.current.copyToClipboard('test')); + expect(result.current.isCopied).toBe(false); + }); + it('should handle exceptions from navigator.clipboard.writeText', async () => { + const error = new Error('Failed to copy'); + navigator.clipboard.writeText = jest.fn().mockImplementation(() => Promise.reject(error)); + const { result } = renderHook(() => useCopyToClipboard()); + const consoleErrorSpy = jest.spyOn(console, 'error'); + expect(consoleErrorSpy).toHaveBeenCalledTimes(0); + await act(async () => result.current.copyToClipboard('test')); + expect(consoleErrorSpy).toHaveBeenCalled(); + }); + + it('should handle exceptions from document.execCommand', async () => { + window.isSecureContext = false; + document.execCommand = jest.fn().mockImplementation(() => { throw new Error('Failed to copy'); }); + const { result } = renderHook(() => useCopyToClipboard()); + const consoleErrorSpy = jest.spyOn(console, 'error'); + expect(consoleErrorSpy).toHaveBeenCalledTimes(0); + await act(async () => result.current.copyToClipboard('test')); + expect(consoleErrorSpy).toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/frontend/packages/core/__tests__/hooks/crypto.test.ts b/frontend/packages/core/__tests__/hooks/crypto.test.ts new file mode 100644 index 00000000..68668586 --- /dev/null +++ b/frontend/packages/core/__tests__/hooks/crypto.test.ts @@ -0,0 +1,29 @@ + +import {useCrypto} from '../../src/hooks/crypto'; +import CryptoJS from 'crypto-js'; + +describe('useCrypto', () => { + const { encryptByEnAES } = useCrypto(); + const key = '1e42=7838a1vfc6n'; + const data = 'test data'; + const iv = '1234567890123456' + + it('should return correct ciphertext', () => { + const encryptedData = encryptByEnAES(key, data, iv); + const tmpKey = CryptoJS.enc.Latin1.parse(CryptoJS.MD5(key).toString() || ''); + const tmpIv = CryptoJS.enc.Latin1.parse(iv); + const decryptedData = CryptoJS.AES.decrypt( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + { ciphertext: CryptoJS.enc.Base64.parse(encryptedData)} as any, + tmpKey, + { + iv: tmpIv, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7, + } + ).toString(CryptoJS.enc.Utf8); + + expect(decryptedData).toBe(data); + }); + +}); \ No newline at end of file diff --git a/frontend/packages/core/__tests__/hooks/http.test.ts b/frontend/packages/core/__tests__/hooks/http.test.ts new file mode 100644 index 00000000..c191bc7e --- /dev/null +++ b/frontend/packages/core/__tests__/hooks/http.test.ts @@ -0,0 +1,93 @@ + +import { useFetch } from '../../src/hooks/http'; +import fetchMock from 'jest-fetch-mock'; + +fetchMock.enableMocks(); + +describe('useFetch', () => { + beforeEach(() => { + fetchMock.resetMocks(); + }); + + it('fetchData should return data correctly', async () => { + const { fetchData } = useFetch(); + const mockData = { key: 'value' }; + fetchMock.mockResponseOnce(JSON.stringify(mockData)); + + const data = await fetchData('test.com', { method: 'GET' }); + + expect(data).toEqual(mockData); + expect(fetchMock.mock.calls.length).toEqual(1); + expect(fetchMock.mock.calls[0][0]).toEqual('/api/v1/test.com'); + }); + + it('fetchData should handle server error', async () => { + const { fetchData } = useFetch(); + fetchMock.mockReject(() => Promise.reject('API is down')); + + await expect(fetchData('test.com', { method: 'GET' })).rejects.toMatch('API is down'); + expect(fetchMock.mock.calls.length).toEqual(1); + expect(fetchMock.mock.calls[0][0]).toEqual('/api/v1/test.com'); + }); + + it('fetchData should handle 401 error', async () => { + const { fetchData } = useFetch(); + fetchMock.mockResponseOnce(JSON.stringify({ message: 'Unauthorized' }), { status: 401 }); + + // Mocking a function to simulate redirect to login page + const mockRedirectToLogin = jest.fn(); + global.window.location.assign = mockRedirectToLogin; + + await expect(fetchData('test.com', { method: 'GET' })).rejects.toThrow('Unauthorized'); + + expect(mockRedirectToLogin).toHaveBeenCalledWith('/login'); + expect(fetchMock.mock.calls.length).toEqual(1); + expect(fetchMock.mock.calls[0][0]).toEqual('/api/v1/test.com'); + }); + + it('fetchData should handle 403 error', async () => { + const { fetchData } = useFetch(); + fetchMock.mockResponseOnce(JSON.stringify({ message: 'Forbidden' }), { status: 403 }); + + await expect(fetchData('test.com', { method: 'GET' })).rejects.toThrow('Forbidden'); + expect(fetchMock.mock.calls.length).toEqual(1); + expect(fetchMock.mock.calls[0][0]).toEqual('/api/v1/test.com'); + }); + + it('should handle camel case to snake case conversion in URL', async () => { + const { fetchData } = useFetch(); + fetchMock.mockResponseOnce(JSON.stringify({})); + + await fetchData('testUrl', { method: 'GET' }); + + expect(fetchMock.mock.calls[0][0]).toEqual('/api/v1/test_url'); + }); + + it('should handle snake case to camel case conversion in response', async () => { + const { fetchData } = useFetch(); + const mockData = { test_key: 'value' }; + fetchMock.mockResponseOnce(JSON.stringify(mockData)); + + const data = await fetchData('testUrl', { method: 'GET' }); + + expect(data).toEqual({ testKey: 'value' }); + }); + + it('should not transform URL if it is in the whitelist', async () => { + const { fetchData } = useFetch(); + fetchMock.mockResponseOnce(JSON.stringify({})); + + await fetchData('api.example.com/users', { method: 'GET' }); + + expect(fetchMock.mock.calls[0][0]).toEqual('/api/v1/api.example.com/users'); + }); + + it('should remove extra white spaces in URL', async () => { + const { fetchData } = useFetch(); + fetchMock.mockResponseOnce(JSON.stringify({})); + + await fetchData(' test Url ', { method: 'GET' }); + + expect(fetchMock.mock.calls[0][0]).toEqual('/api/v1/test_url'); + }); +}); \ No newline at end of file diff --git a/frontend/packages/core/index.html b/frontend/packages/core/index.html new file mode 100644 index 00000000..cb9482b5 --- /dev/null +++ b/frontend/packages/core/index.html @@ -0,0 +1,16 @@ + + + + + + + + APIPark - 企业API数据开放平台 + + +
+ + + + + diff --git a/frontend/packages/core/package.json b/frontend/packages/core/package.json new file mode 100644 index 00000000..25f04f21 --- /dev/null +++ b/frontend/packages/core/package.json @@ -0,0 +1,24 @@ +{ + "name": "core", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": " vite --mode open --port 5000 --strictPort", + "dev:pro": " vite --config ./vite.pro.config.ts --mode pro --port 5000 --strictPort ", + "build": "vite build --mode open", + "build:pro": "vite --config ./vite.pro.config.ts build --mode pro", + "postinstall": "node scripts/moveTinymce.js", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview --port 5000 --strictPort", + "serve": "vite preview --port 5000 --strictPort" + }, + "dependencies": { + "@tinymce/tinymce-react": "^4.3.2", + "tinymce": "^6.8.1", + "highlight.js": "^11.9.0", + "fs-extra": "^11.2.0" + }, + "devDependencies": { + } +} diff --git a/frontend/packages/core/postcss.config.js b/frontend/packages/core/postcss.config.js new file mode 100644 index 00000000..69215941 --- /dev/null +++ b/frontend/packages/core/postcss.config.js @@ -0,0 +1,15 @@ +/* + * @Date: 2023-11-27 17:31:54 + * @LastEditors: maggieyyy + * @LastEditTime: 2024-06-05 10:42:18 + * @FilePath: \frontend\packages\core\postcss.config.js + */ +export default { + plugins: { + 'postcss-import': {}, + 'tailwindcss/nesting': {}, + tailwindcss: {}, + autoprefixer: {} + }, + } + \ No newline at end of file diff --git a/frontend/packages/core/public/favicon.ico b/frontend/packages/core/public/favicon.ico new file mode 100644 index 00000000..2c1de84c Binary files /dev/null and b/frontend/packages/core/public/favicon.ico differ diff --git a/frontend/packages/core/public/iconpark_apinto.js b/frontend/packages/core/public/iconpark_apinto.js new file mode 100644 index 00000000..d125a4a0 --- /dev/null +++ b/frontend/packages/core/public/iconpark_apinto.js @@ -0,0 +1,8 @@ +/* + * @Date: 2024-05-06 09:47:27 + * @LastEditors: maggieyyy + * @LastEditTime: 2024-05-06 09:47:47 + * @FilePath: \frontend\packages\core\src\assets\iconpark_apinto.js + */ +(function(){window.__iconpark__=window.__iconpark__||{};var obj=JSON.parse("{\"680840\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"none\",\"content\":\"\"},\"680856\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680857\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680858\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680859\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680860\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680861\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680862\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680863\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680864\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680865\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680866\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680867\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680868\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680869\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680870\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680871\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680872\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680873\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680874\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680875\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680876\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680877\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680878\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680879\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680880\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680881\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680882\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680883\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680884\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680885\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680886\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680887\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680888\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680889\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680890\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680891\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680892\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680893\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680894\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680895\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680896\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680897\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680898\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680899\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680900\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680901\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680902\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680903\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680904\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680905\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680906\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680907\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680908\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680909\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680910\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680911\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680912\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680913\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680914\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680915\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680916\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680917\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680918\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680919\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680920\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680921\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680922\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680923\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680924\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680925\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680926\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680927\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680928\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680929\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680930\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680931\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680932\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680933\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680934\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680935\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680936\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680937\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680938\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"680939\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"681014\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"681015\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"681016\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"681017\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"681018\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"681019\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"currentColor\",\"content\":\"\"},\"681787\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"none\",\"content\":\"\"},\"694557\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"694558\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"707431\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"707736\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"707739\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"707741\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"707742\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"707743\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"707744\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"707749\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"708142\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"708144\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"708145\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"708146\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"708147\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"708181\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"709715\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"808898\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"808900\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"808916\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"810363\":{\"viewBox\":\"0 0 20 20\",\"fill\":\"none\",\"content\":\"\"},\"810396\":{\"viewBox\":\"0 0 20 20\",\"fill\":\"none\",\"content\":\"\"},\"818250\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"818340\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"none\",\"content\":\"\"}}");for(var _k in obj){window.__iconpark__[_k] = obj[_k]};var nm={"fuzhi1":680840,"bianji":680856,"tishi":680857,"xinkaibiaoqian":680858,"bianjiyoujian":680859,"suoyou":680860,"fuzhi":680861,"shijian":680862,"gundongxuanze":680863,"guanbi":680864,"zanting":680865,"chenggong":680866,"chakanyinyong":680867,"guidang":680868,"tihuan":680869,"zhankai":680870,"duankailianjie":680871,"Cookieguanli":680872,"riqi":680873,"zhongwen":680874,"yingwen":680875,"shuaxinjiankongzhuangtai":680876,"rili":680877,"genmulu":680878,"shangxianguanli":680879,"daimashili":680880,"yingyong":680881,"tianjia-2":680882,"qingchu":680883,"daoru":680884,"xiala":680885,"kuaisuceshi-2":680886,"saoyisao":680887,"shiyongjiaocheng":680888,"tianjiafujian":680889,"xiazai":680890,"jinzhidengji":680891,"fasongyoujian":680892,"bug":680893,"chaping":680894,"kuaisuceshi":680895,"yunshangchuan":680896,"yunxiazai":680897,"sousuo":680898,"peizhi":680899,"xinchuangkoudakai":680900,"shanchu-2":680901,"quanjusuoxiao":680902,"qiehuan":680903,"jianshao":680904,"chakanAPIlishi":680905,"lianjie":680906,"dunpaibaoxianrenzheng":680907,"shaixuan":680908,"zhihang":680909,"congmobanzhongchuangjianyongli":680910,"zhengligeshi":680911,"haoping":680912,"shouqi-2":680913,"gouwuche":680914,"lishi":680915,"tianjiaziji":680916,"tianjia":680917,"shouqi":680918,"gailan":680919,"paixu":680920,"gengduo":680921,"guanliyuanrenzheng":680922,"fenxiang":680923,"shanchu":680924,"yidong":680925,"chakan":680926,"shujuku":680927,"shangyoufuwu-":680928,"huanyuangeshi":680929,"jiangxu":680930,"shengxu":680931,"quanjufangda":680932,"xuanzhong":680933,"riqiqujian":680934,"ditu-1":680935,"qunzu":680936,"daochu":680937,"zhankai-":680938,"ditu-2":680939,"Eolink":681014,"APISpace":681015,"Apinto":681016,"json":681017,"webhook":681018,"linux":681019,"xinzengfenzu":681787,"circle-right-up":694557,"circle-right-down":694558,"APIjiekou-7mme3dcg":707431,"connection-box":707736,"system":707739,"form-one":707741,"yingyong-7mmhj11e":707742,"jiankongshexiangtou":707743,"file-cabinet":707744,"network-tree":707749,"search":708142,"find":708144,"circle-right-up-7mnlo5g9":708145,"circle-right-down-7mnlphn2":708146,"reduce-one":708147,"tool":708181,"shangxianguanli-new":709715,"hamburger-button":808898,"puzzle":808900,"keyline":808916,"daohang":810363,"lanjieqiguanli":810396,"shop":818250,"xiangmu":818340};for(var _i in nm){window.__iconpark__[_i] = obj[nm[_i]]}})();"object"!=typeof globalThis&&(Object.prototype.__defineGetter__("__magic__",function(){return this}),__magic__.globalThis=__magic__,delete Object.prototype.__magic__);(()=>{"use strict";var t={816:(t,e,i)=>{var s,r,o,n;i.d(e,{Vm:()=>z,dy:()=>P,Jb:()=>x,Ld:()=>$,sY:()=>T,YP:()=>A});const l=globalThis.trustedTypes,a=l?l.createPolicy("lit-html",{createHTML:t=>t}):void 0,h=`lit$${(Math.random()+"").slice(9)}$`,c="?"+h,d=`<${c}>`,u=document,p=(t="")=>u.createComment(t),v=t=>null===t||"object"!=typeof t&&"function"!=typeof t,f=Array.isArray,y=t=>{var e;return f(t)||"function"==typeof(null===(e=t)||void 0===e?void 0:e[Symbol.iterator])},m=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,g=/-->/g,b=/>/g,S=/>|[ \n \r](?:([^\s"'>=/]+)([ \n \r]*=[ \n \r]*(?:[^ \n \r"'`<>=]|("|')|))|$)/g,w=/'/g,k=/"/g,E=/^(?:script|style|textarea)$/i,C=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),P=C(1),A=C(2),x=Symbol.for("lit-noChange"),$=Symbol.for("lit-nothing"),O=new WeakMap,T=(t,e,i)=>{var s,r;const o=null!==(s=null==i?void 0:i.renderBefore)&&void 0!==s?s:e;let n=o._$litPart$;if(void 0===n){const t=null!==(r=null==i?void 0:i.renderBefore)&&void 0!==r?r:null;o._$litPart$=n=new H(e.insertBefore(p(),t),t,void 0,i)}return n.I(t),n},R=u.createTreeWalker(u,129,null,!1),_=(t,e)=>{const i=t.length-1,s=[];let r,o=2===e?"":"",n=m;for(let e=0;e"===a[0]?(n=null!=r?r:m,c=-1):void 0===a[1]?c=-2:(c=n.lastIndex-a[2].length,l=a[1],n=void 0===a[3]?S:'"'===a[3]?k:w):n===k||n===w?n=S:n===g||n===b?n=m:(n=S,r=void 0);const p=n===S&&t[e+1].startsWith("/>")?" ":"";o+=n===m?i+d:c>=0?(s.push(l),i.slice(0,c)+"$lit$"+i.slice(c)+h+p):i+h+(-2===c?(s.push(void 0),e):p)}const l=o+(t[i]||"")+(2===e?"":"");return[void 0!==a?a.createHTML(l):l,s]};class N{constructor({strings:t,_$litType$:e},i){let s;this.parts=[];let r=0,o=0;const n=t.length-1,a=this.parts,[d,u]=_(t,e);if(this.el=N.createElement(d,i),R.currentNode=this.el.content,2===e){const t=this.el.content,e=t.firstChild;e.remove(),t.append(...e.childNodes)}for(;null!==(s=R.nextNode())&&a.length0){s.textContent=l?l.emptyScript:"";for(let i=0;i2||""!==i[0]||""!==i[1]?(this.H=Array(i.length-1).fill($),this.strings=i):this.H=$}get tagName(){return this.element.tagName}I(t,e=this,i,s){const r=this.strings;let o=!1;if(void 0===r)t=U(this,t,e,0),o=!v(t)||t!==this.H&&t!==x,o&&(this.H=t);else{const s=t;let n,l;for(t=r[0],n=0;n{i.r(e),i.d(e,{customElement:()=>s,eventOptions:()=>a,property:()=>o,query:()=>h,queryAll:()=>c,queryAssignedNodes:()=>v,queryAsync:()=>d,state:()=>n});const s=t=>e=>"function"==typeof e?((t,e)=>(window.customElements.define(t,e),e))(t,e):((t,e)=>{const{kind:i,elements:s}=e;return{kind:i,elements:s,finisher(e){window.customElements.define(t,e)}}})(t,e),r=(t,e)=>"method"===e.kind&&e.descriptor&&!("value"in e.descriptor)?{...e,finisher(i){i.createProperty(e.key,t)}}:{kind:"field",key:Symbol(),placement:"own",descriptor:{},originalKey:e.key,initializer(){"function"==typeof e.initializer&&(this[e.key]=e.initializer.call(this))},finisher(i){i.createProperty(e.key,t)}};function o(t){return(e,i)=>void 0!==i?((t,e,i)=>{e.constructor.createProperty(i,t)})(t,e,i):r(t,e)}function n(t){return o({...t,state:!0,attribute:!1})}const l=({finisher:t,descriptor:e})=>(i,s)=>{var r;if(void 0===s){const s=null!==(r=i.originalKey)&&void 0!==r?r:i.key,o=null!=e?{kind:"method",placement:"prototype",key:s,descriptor:e(i.key)}:{...i,key:s};return null!=t&&(o.finisher=function(e){t(e,s)}),o}{const r=i.constructor;void 0!==e&&Object.defineProperty(i,s,e(s)),null==t||t(r,s)}};function a(t){return l({finisher:(e,i)=>{Object.assign(e.prototype[i],t)}})}function h(t,e){return l({descriptor:i=>{const s={get(){var e;return null===(e=this.renderRoot)||void 0===e?void 0:e.querySelector(t)},enumerable:!0,configurable:!0};if(e){const e="symbol"==typeof i?Symbol():"__"+i;s.get=function(){var i;return void 0===this[e]&&(this[e]=null===(i=this.renderRoot)||void 0===i?void 0:i.querySelector(t)),this[e]}}return s}})}function c(t){return l({descriptor:e=>({get(){var e;return null===(e=this.renderRoot)||void 0===e?void 0:e.querySelectorAll(t)},enumerable:!0,configurable:!0})})}function d(t){return l({descriptor:e=>({async get(){var e;return await this.updateComplete,null===(e=this.renderRoot)||void 0===e?void 0:e.querySelector(t)},enumerable:!0,configurable:!0})})}const u=Element.prototype,p=u.msMatchesSelector||u.webkitMatchesSelector;function v(t="",e=!1,i=""){return l({descriptor:s=>({get(){var s,r;const o="slot"+(t?`[name=${t}]`:":not([name])");let n=null===(r=null===(s=this.renderRoot)||void 0===s?void 0:s.querySelector(o))||void 0===r?void 0:r.assignedNodes({flatten:e});return n&&i&&(n=n.filter((t=>t.nodeType===Node.ELEMENT_NODE&&(t.matches?t.matches(i):p.call(t,i))))),n},enumerable:!0,configurable:!0})})}},23:(t,e,i)=>{i.r(e),i.d(e,{unsafeSVG:()=>l});const s=t=>(...e)=>({_$litDirective$:t,values:e});var r=i(816);class o extends class{constructor(t){}T(t,e,i){this.Σdt=t,this.M=e,this.Σct=i}S(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}}{constructor(t){if(super(t),this.vt=r.Ld,2!==t.type)throw Error(this.constructor.directiveName+"() can only be used in child bindings")}render(t){if(t===r.Ld)return this.Vt=void 0,this.vt=t;if(t===r.Jb)return t;if("string"!=typeof t)throw Error(this.constructor.directiveName+"() called with a non-string value");if(t===this.vt)return this.Vt;this.vt=t;const e=[t];return e.raw=e,this.Vt={_$litType$:this.constructor.resultType,strings:e,values:[]}}}o.directiveName="unsafeHTML",o.resultType=1,s(o);class n extends o{}n.directiveName="unsafeSVG",n.resultType=2;const l=s(n)},249:(t,e,i)=>{i.r(e),i.d(e,{CSSResult:()=>n,LitElement:()=>x,ReactiveElement:()=>b,UpdatingElement:()=>A,_Σ:()=>s.Vm,_Φ:()=>$,adoptStyles:()=>c,css:()=>h,defaultConverter:()=>y,getCompatibleStyle:()=>d,html:()=>s.dy,noChange:()=>s.Jb,notEqual:()=>m,nothing:()=>s.Ld,render:()=>s.sY,supportsAdoptingStyleSheets:()=>r,svg:()=>s.YP,unsafeCSS:()=>l});var s=i(816);const r=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,o=Symbol();class n{constructor(t,e){if(e!==o)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t}get styleSheet(){return r&&void 0===this.t&&(this.t=new CSSStyleSheet,this.t.replaceSync(this.cssText)),this.t}toString(){return this.cssText}}const l=t=>new n(t+"",o),a=new Map,h=(t,...e)=>{const i=e.reduce(((e,i,s)=>e+(t=>{if(t instanceof n)return t.cssText;if("number"==typeof t)return t;throw Error(`Value passed to 'css' function must be a 'css' function result: ${t}. Use 'unsafeCSS' to pass non-literal values, but\n take care to ensure page security.`)})(i)+t[s+1]),t[0]);let s=a.get(i);return void 0===s&&a.set(i,s=new n(i,o)),s},c=(t,e)=>{r?t.adoptedStyleSheets=e.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):e.forEach((e=>{const i=document.createElement("style");i.textContent=e.cssText,t.appendChild(i)}))},d=r?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return l(e)})(t):t;var u,p,v,f;const y={toAttribute(t,e){switch(e){case Boolean:t=t?"":null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},m=(t,e)=>e!==t&&(e==e||t==t),g={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:m};class b extends HTMLElement{constructor(){super(),this.Πi=new Map,this.Πo=void 0,this.Πl=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this.Πh=null,this.u()}static addInitializer(t){var e;null!==(e=this.v)&&void 0!==e||(this.v=[]),this.v.push(t)}static get observedAttributes(){this.finalize();const t=[];return this.elementProperties.forEach(((e,i)=>{const s=this.Πp(i,e);void 0!==s&&(this.Πm.set(s,i),t.push(s))})),t}static createProperty(t,e=g){if(e.state&&(e.attribute=!1),this.finalize(),this.elementProperties.set(t,e),!e.noAccessor&&!this.prototype.hasOwnProperty(t)){const i="symbol"==typeof t?Symbol():"__"+t,s=this.getPropertyDescriptor(t,i,e);void 0!==s&&Object.defineProperty(this.prototype,t,s)}}static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(s){const r=this[t];this[e]=s,this.requestUpdate(t,r,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||g}static finalize(){if(this.hasOwnProperty("finalized"))return!1;this.finalized=!0;const t=Object.getPrototypeOf(this);if(t.finalize(),this.elementProperties=new Map(t.elementProperties),this.Πm=new Map,this.hasOwnProperty("properties")){const t=this.properties,e=[...Object.getOwnPropertyNames(t),...Object.getOwnPropertySymbols(t)];for(const i of e)this.createProperty(i,t[i])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(d(t))}else void 0!==t&&e.push(d(t));return e}static Πp(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}u(){var t;this.Πg=new Promise((t=>this.enableUpdating=t)),this.L=new Map,this.Π_(),this.requestUpdate(),null===(t=this.constructor.v)||void 0===t||t.forEach((t=>t(this)))}addController(t){var e,i;(null!==(e=this.ΠU)&&void 0!==e?e:this.ΠU=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(i=t.hostConnected)||void 0===i||i.call(t))}removeController(t){var e;null===(e=this.ΠU)||void 0===e||e.splice(this.ΠU.indexOf(t)>>>0,1)}Π_(){this.constructor.elementProperties.forEach(((t,e)=>{this.hasOwnProperty(e)&&(this.Πi.set(e,this[e]),delete this[e])}))}createRenderRoot(){var t;const e=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return c(e,this.constructor.elementStyles),e}connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this.ΠU)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostConnected)||void 0===e?void 0:e.call(t)})),this.Πl&&(this.Πl(),this.Πo=this.Πl=void 0)}enableUpdating(t){}disconnectedCallback(){var t;null===(t=this.ΠU)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostDisconnected)||void 0===e?void 0:e.call(t)})),this.Πo=new Promise((t=>this.Πl=t))}attributeChangedCallback(t,e,i){this.K(t,i)}Πj(t,e,i=g){var s,r;const o=this.constructor.Πp(t,i);if(void 0!==o&&!0===i.reflect){const n=(null!==(r=null===(s=i.converter)||void 0===s?void 0:s.toAttribute)&&void 0!==r?r:y.toAttribute)(e,i.type);this.Πh=t,null==n?this.removeAttribute(o):this.setAttribute(o,n),this.Πh=null}}K(t,e){var i,s,r;const o=this.constructor,n=o.Πm.get(t);if(void 0!==n&&this.Πh!==n){const t=o.getPropertyOptions(n),l=t.converter,a=null!==(r=null!==(s=null===(i=l)||void 0===i?void 0:i.fromAttribute)&&void 0!==s?s:"function"==typeof l?l:null)&&void 0!==r?r:y.fromAttribute;this.Πh=n,this[n]=a(e,t.type),this.Πh=null}}requestUpdate(t,e,i){let s=!0;void 0!==t&&(((i=i||this.constructor.getPropertyOptions(t)).hasChanged||m)(this[t],e)?(this.L.has(t)||this.L.set(t,e),!0===i.reflect&&this.Πh!==t&&(void 0===this.Πk&&(this.Πk=new Map),this.Πk.set(t,i))):s=!1),!this.isUpdatePending&&s&&(this.Πg=this.Πq())}async Πq(){this.isUpdatePending=!0;try{for(await this.Πg;this.Πo;)await this.Πo}catch(t){Promise.reject(t)}const t=this.performUpdate();return null!=t&&await t,!this.isUpdatePending}performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,this.Πi&&(this.Πi.forEach(((t,e)=>this[e]=t)),this.Πi=void 0);let e=!1;const i=this.L;try{e=this.shouldUpdate(i),e?(this.willUpdate(i),null===(t=this.ΠU)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostUpdate)||void 0===e?void 0:e.call(t)})),this.update(i)):this.Π$()}catch(t){throw e=!1,this.Π$(),t}e&&this.E(i)}willUpdate(t){}E(t){var e;null===(e=this.ΠU)||void 0===e||e.forEach((t=>{var e;return null===(e=t.hostUpdated)||void 0===e?void 0:e.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}Π$(){this.L=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this.Πg}shouldUpdate(t){return!0}update(t){void 0!==this.Πk&&(this.Πk.forEach(((t,e)=>this.Πj(e,this[e],t))),this.Πk=void 0),this.Π$()}updated(t){}firstUpdated(t){}}var S,w,k,E,C,P;b.finalized=!0,b.shadowRootOptions={mode:"open"},null===(p=(u=globalThis).reactiveElementPlatformSupport)||void 0===p||p.call(u,{ReactiveElement:b}),(null!==(v=(f=globalThis).reactiveElementVersions)&&void 0!==v?v:f.reactiveElementVersions=[]).push("1.0.0-rc.1");const A=b;(null!==(S=(P=globalThis).litElementVersions)&&void 0!==S?S:P.litElementVersions=[]).push("3.0.0-rc.1");class x extends b{constructor(){super(...arguments),this.renderOptions={host:this},this.Φt=void 0}createRenderRoot(){var t,e;const i=super.createRenderRoot();return null!==(t=(e=this.renderOptions).renderBefore)&&void 0!==t||(e.renderBefore=i.firstChild),i}update(t){const e=this.render();super.update(t),this.Φt=(0,s.sY)(e,this.renderRoot,this.renderOptions)}connectedCallback(){var t;super.connectedCallback(),null===(t=this.Φt)||void 0===t||t.setConnected(!0)}disconnectedCallback(){var t;super.disconnectedCallback(),null===(t=this.Φt)||void 0===t||t.setConnected(!1)}render(){return s.Jb}}x.finalized=!0,x._$litElement$=!0,null===(k=(w=globalThis).litElementHydrateSupport)||void 0===k||k.call(w,{LitElement:x}),null===(C=(E=globalThis).litElementPlatformSupport)||void 0===C||C.call(E,{LitElement:x});const $={K:(t,e,i)=>{t.K(e,i)},L:t=>t.L}},409:function(t,e,i){var s=this&&this.__decorate||function(t,e,i,s){var r,o=arguments.length,n=o<3?e:null===s?s=Object.getOwnPropertyDescriptor(e,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,i,s);else for(var l=t.length-1;l>=0;l--)(r=t[l])&&(n=(o<3?r(n):o>3?r(e,i,n):r(e,i))||n);return o>3&&n&&Object.defineProperty(e,i,n),n};Object.defineProperty(e,"__esModule",{value:!0}),e.IconparkIconElement=void 0;const r=i(249),o=i(26),n=i(23),l={color:1,fill:1,stroke:1},a={STROKE:{trackAttr:"data-follow-stroke",rawAttr:"stroke"},FILL:{trackAttr:"data-follow-fill",rawAttr:"fill"}};class h extends r.LitElement{constructor(){super(...arguments),this.name="",this.identifyer="",this.size="1em"}get _width(){return this.width||this.size}get _height(){return this.height||this.size}get _stroke(){return this.stroke||this.color}get _fill(){return this.fill||this.color}get SVGConfig(){return(window.__iconpark__||{})[this.identifyer]||(window.__iconpark__||{})[this.name]||{viewBox:"0 0 0 0",content:""}}connectedCallback(){super.connectedCallback(),setTimeout((()=>{this.monkeyPatch("STROKE",!0),this.monkeyPatch("FILL",!0)}))}monkeyPatch(t,e){switch(t){case"STROKE":this.updateDOMByHand(this.strokeAppliedNodes,"STROKE",this._stroke,!!e);break;case"FILL":this.updateDOMByHand(this.fillAppliedNodes,"FILL",this._fill,!!e)}}updateDOMByHand(t,e,i,s){!i&&s||t&&t.forEach((t=>{i&&i===t.getAttribute(a[e].rawAttr)||t.setAttribute(a[e].rawAttr,i||t.getAttribute(a[e].trackAttr))}))}attributeChangedCallback(t,e,i){super.attributeChangedCallback(t,e,i),"name"===t||"identifyer"===t?setTimeout((()=>{this.monkeyPatch("STROKE"),this.monkeyPatch("FILL")})):l[t]&&(this.monkeyPatch("STROKE"),this.monkeyPatch("FILL"))}render(){return r.svg`${n.unsafeSVG(this.SVGConfig.content)}`}}h.styles=r.css`:host {display: inline-flex; align-items: center; justify-content: center;} :host([spin]) svg {animation: iconpark-spin 1s infinite linear;} :host([spin][rtl]) svg {animation: iconpark-spin-rtl 1s infinite linear;} :host([rtl]) svg {transform: scaleX(-1);} @keyframes iconpark-spin {0% { -webkit-transform: rotate(0); transform: rotate(0);} 100% {-webkit-transform: rotate(360deg); transform: rotate(360deg);}} @keyframes iconpark-spin-rtl {0% {-webkit-transform: scaleX(-1) rotate(0); transform: scaleX(-1) rotate(0);} 100% {-webkit-transform: scaleX(-1) rotate(360deg); transform: scaleX(-1) rotate(360deg);}}`,s([o.property({reflect:!0})],h.prototype,"name",void 0),s([o.property({reflect:!0,attribute:"icon-id"})],h.prototype,"identifyer",void 0),s([o.property({reflect:!0})],h.prototype,"color",void 0),s([o.property({reflect:!0})],h.prototype,"stroke",void 0),s([o.property({reflect:!0})],h.prototype,"fill",void 0),s([o.property({reflect:!0})],h.prototype,"size",void 0),s([o.property({reflect:!0})],h.prototype,"width",void 0),s([o.property({reflect:!0})],h.prototype,"height",void 0),s([o.queryAll(`[${a.STROKE.trackAttr}]`)],h.prototype,"strokeAppliedNodes",void 0),s([o.queryAll(`[${a.FILL.trackAttr}]`)],h.prototype,"fillAppliedNodes",void 0),e.IconparkIconElement=h,customElements.get("iconpark-icon")||customElements.define("iconpark-icon",h)}},e={};function i(s){var r=e[s];if(void 0!==r)return r.exports;var o=e[s]={exports:{}};return t[s].call(o.exports,o,o.exports,i),o.exports}i.d=(t,e)=>{for(var s in e)i.o(e,s)&&!i.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:e[s]})},i.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),i.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},i(409)})(); + \ No newline at end of file diff --git a/frontend/packages/core/public/iconpark_eolink.js b/frontend/packages/core/public/iconpark_eolink.js new file mode 100644 index 00000000..5c7f6392 --- /dev/null +++ b/frontend/packages/core/public/iconpark_eolink.js @@ -0,0 +1,7 @@ +/* + * @Date: 2024-05-06 09:53:45 + * @LastEditors: maggieyyy + * @LastEditTime: 2024-05-06 09:53:50 + * @FilePath: \frontend\packages\core\src\assets\iconpark_eolink.js + */ +(function(){window.__iconpark__=window.__iconpark__||{};var obj=JSON.parse("{\"647367\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"684408\":{\"viewBox\":\"0 0 194 194\",\"content\":\"\"},\"684409\":{\"viewBox\":\"0 0 194 194\",\"content\":\"\"},\"684411\":{\"viewBox\":\"0 0 119.19 102.5\",\"content\":\"\"},\"684412\":{\"viewBox\":\"0 0 108.55 93.99\",\"fill\":\"currentColor\",\"content\":\"\"},\"684413\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"684414\":{\"viewBox\":\"0 0 1024 1024\",\"fill\":\"currentColor\",\"content\":\"\"},\"686740\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686741\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686742\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686743\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686744\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686745\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686746\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686747\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686748\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686749\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686750\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686751\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686752\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686753\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686754\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"686993\":{\"viewBox\":\"0 0 38.22 22.18\",\"fill\":\"currentColor\",\"content\":\"\"},\"687741\":{\"viewBox\":\"0 0 194 194\",\"content\":\"\"},\"687742\":{\"viewBox\":\"0 0 194 194\",\"content\":\"\"},\"691262\":{\"viewBox\":\"0 0 194 194\",\"content\":\"\"},\"691537\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"691538\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"691806\":{\"viewBox\":\"0 0 194 194\",\"content\":\"\"},\"695738\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695739\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695740\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695741\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695742\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695743\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695746\":{\"viewBox\":\"0 0 1185 1024\",\"fill\":\"currentColor\",\"content\":\"\"},\"695747\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695748\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695750\":{\"viewBox\":\"0 0 1024 1024\",\"fill\":\"currentColor\",\"content\":\"\"},\"695751\":{\"viewBox\":\"0 0 1024 1024\",\"fill\":\"currentColor\",\"content\":\"\"},\"695752\":{\"viewBox\":\"0 0 1024 1024\",\"fill\":\"currentColor\",\"content\":\"\"},\"695754\":{\"viewBox\":\"0 0 1024 1024\",\"fill\":\"currentColor\",\"content\":\"\"},\"695755\":{\"viewBox\":\"0 0 1024 1024\",\"fill\":\"currentColor\",\"content\":\"\"},\"695756\":{\"viewBox\":\"0 0 1024 1024\",\"fill\":\"currentColor\",\"content\":\"\"},\"695758\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695759\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695760\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695761\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695762\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695763\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695764\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695801\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695802\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695803\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695804\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695805\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695806\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695807\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695810\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695811\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695812\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695817\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695818\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695819\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695820\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695821\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695822\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695828\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695829\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695830\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695831\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695833\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695834\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695835\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695836\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695837\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695838\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695839\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695840\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695841\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695842\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695844\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695845\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695846\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695865\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695867\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695868\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695869\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695870\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695876\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695877\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695878\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695883\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695884\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695886\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695887\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695888\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695889\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695890\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695891\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695892\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695893\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695896\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695899\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695900\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695901\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695902\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695903\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695904\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695905\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695906\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695907\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695908\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695909\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695913\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695914\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695915\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695916\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695933\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695934\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695935\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695936\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695938\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695940\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695941\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695942\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695944\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695945\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695946\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695947\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695948\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695950\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695951\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695953\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695954\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695955\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695956\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695957\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695958\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695959\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695960\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695961\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695962\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695963\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695964\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695966\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695967\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695968\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695969\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695971\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695972\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695973\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695975\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695978\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695979\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695980\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695981\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695982\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695984\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695985\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695986\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695987\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695988\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695990\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695993\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695995\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695997\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"695999\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696002\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696003\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696004\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696005\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696007\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696009\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696010\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696011\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696012\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696013\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696014\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696015\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696016\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696017\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696018\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696019\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696020\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696021\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696022\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696023\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696024\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696025\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696027\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696028\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696029\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696030\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696031\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696032\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696033\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696034\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696035\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696036\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696037\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696038\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696039\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696040\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696041\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696042\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696043\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696044\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696045\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696046\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696048\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696049\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696660\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"696661\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"744163\":{\"viewBox\":\"0 0 1024 1024\",\"fill\":\"currentColor\",\"content\":\"\"},\"744173\":{\"viewBox\":\"0 0 128 128\",\"fill\":\"none\",\"content\":\"\"},\"744175\":{\"viewBox\":\"0 0 128 128\",\"fill\":\"none\",\"content\":\"\"},\"750656\":{\"viewBox\":\"0 0 61 61\",\"fill\":\"none\",\"content\":\"\"},\"752737\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"756392\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"757321\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"757499\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"757504\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"757518\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"757519\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"757520\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"757521\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"757616\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"757650\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"767277\":{\"viewBox\":\"0 0 20 20\",\"fill\":\"none\",\"content\":\"\"},\"767278\":{\"viewBox\":\"0 0 20 20\",\"fill\":\"none\",\"content\":\"\"},\"775549\":{\"viewBox\":\"0 0 18 14\",\"fill\":\"none\",\"content\":\"\"},\"779333\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"none\",\"content\":\"\"},\"779418\":{\"viewBox\":\"0 0 1024 1024\",\"content\":\"\"},\"779705\":{\"viewBox\":\"0 0 20 20\",\"fill\":\"none\",\"content\":\"\"},\"779706\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"none\",\"content\":\"\"},\"787702\":{\"viewBox\":\"0 0 1024 1024\",\"content\":\"\"},\"788577\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"802334\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"804269\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"804612\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"804614\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"806103\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"813707\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"815901\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"820089\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"826687\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"854318\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"855246\":{\"viewBox\":\"0 0 16 16\",\"fill\":\"none\",\"content\":\"\"},\"855247\":{\"viewBox\":\"0 0 16 16\",\"fill\":\"none\",\"content\":\"\"},\"855248\":{\"viewBox\":\"0 0 16 16\",\"fill\":\"none\",\"content\":\"\"},\"855927\":{\"viewBox\":\"0 0 83 20\",\"fill\":\"none\",\"content\":\"\"},\"855928\":{\"viewBox\":\"0 0 68 24\",\"fill\":\"none\",\"content\":\"\"},\"855929\":{\"viewBox\":\"0 0 66 24\",\"fill\":\"none\",\"content\":\"\"},\"855938\":{\"viewBox\":\"0 0 198 72\",\"fill\":\"none\",\"content\":\"\"},\"857931\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"857985\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"861388\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"876705\":{\"viewBox\":\"0 0 16 16\",\"fill\":\"none\",\"content\":\"\"},\"884011\":{\"viewBox\":\"0 0 20 20\",\"fill\":\"none\",\"content\":\"\"},\"885387\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"897026\":{\"viewBox\":\"0 0 250 250\",\"fill\":\"none\",\"content\":\"\"},\"915485\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"929257\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"932197\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"949128\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"970590\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"973801\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"985435\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1002903\":{\"viewBox\":\"0 0 24 24\",\"fill\":\"none\",\"content\":\"\"},\"1021623\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1021686\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1035721\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1035737\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1037074\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1037815\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1037816\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1037817\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1039918\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1042170\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"},\"1042171\":{\"viewBox\":\"0 0 48 48\",\"fill\":\"none\",\"content\":\"\"}}");for(var _k in obj){window.__iconpark__[_k] = obj[_k]};var nm={"round-fill":647367,"apinto-pro-icon":684408,"apinto-icon":684409,"apinto-pro":684411,"apinto":684412,"check-circle":684413,"apispace":684414,"auto-generate-api":686740,"compare-api":686741,"multi-protocal":686742,"read-good":686743,"richdoc":686744,"mockapi":686745,"script-support":686746,"diy-test":686747,"send":686748,"stereo-perspective":686749,"automatic-robot":686750,"switch-env":686751,"flash":686752,"chart-pie":686753,"date-drive":686754,"apistudio":686993,"postcat-icon":687741,"postcat":687742,"apistudio-icon":691262,"update-rotation":691537,"page":691538,"apispace-icon":691806,"avatar":695738,"people":695739,"people-minus":695740,"people-plus":695741,"peoples":695742,"user-business":695743,"folder-close-fill":695746,"windows":695747,"github":695748,"qq":695750,"browser-chrome":695751,"linux":695752,"edge":695754,"wechat":695755,"browser":695756,"gitlab":695758,"apple":695759,"alipay":695760,"facebook":695761,"twitter":695762,"paypal":695763,"new-lark":695764,"delete":695801,"return":695802,"search":695803,"import":695804,"export":695805,"add":695806,"add-child":695807,"file-addition":695810,"add-circle":695811,"minus":695812,"close":695817,"close-small":695818,"check-small":695819,"check":695820,"code-terminal":695821,"code":695822,"preview-open":695828,"preview-close":695829,"folder-close":695830,"folder-open":695831,"upload":695833,"download":695834,"copy":695835,"upload-file":695836,"compare":695837,"edit":695838,"share":695839,"share-all":695840,"share-url-fill":695841,"share-url":695842,"back":695844,"back-fill":695845,"share-fill":695846,"sort":695865,"filter":695867,"reduce":695868,"done-all":695869,"full-selection":695870,"right-bar":695876,"left-bar":695877,"direction-adjustment":695878,"down-small":695883,"left-small":695884,"right-small":695886,"right-one":695887,"right":695888,"up":695889,"up-one":695890,"up-small":695891,"up-two":695892,"down-two":695893,"enter":695896,"down":695899,"left":695900,"down-one":695901,"left-two":695902,"right-two":695903,"left-one":695904,"more":695905,"expand-left":695906,"expand-right":695907,"column":695908,"center-alignment":695909,"list-add":695913,"sort-amount-down":695914,"sort-amount-up":695915,"list":695916,"remind":695933,"close-remind":695934,"api":695935,"rocket":695936,"monitor":695938,"robot":695940,"plan":695941,"application":695942,"chart-proportion":695944,"data":695945,"chart-line":695946,"pie-10":695947,"pie":695948,"chart-bubble":695950,"cube":695951,"application-menu":695953,"crown":695954,"crown-fill":695955,"market":695956,"file-word":695957,"file-excel":695958,"hashtag-key":695959,"file-hash":695960,"refresh":695961,"order":695962,"command":695963,"branch":695964,"page-template":695966,"smart-optimization":695967,"assembly-line":695968,"stopwatch":695969,"checklist":695971,"menu-fold":695972,"menu-unfold":695973,"alarm":695975,"protection":695978,"caution":695979,"openapi":695980,"webhook":695981,"holding-hands":695982,"support":695984,"agreement":695985,"community":695986,"roadmap":695987,"family-7knl2ae1":695988,"smiling-face":695990,"play-fill":695993,"play":695995,"pause":695997,"magic":695999,"whole-site-accelerator":696002,"link-cloud-faild":696003,"link-cloud-sucess":696004,"translate":696005,"funds":696007,"unhappy-face":696009,"message":696010,"connection-arrow":696011,"loading":696012,"fork":696013,"quote":696014,"headset":696015,"attention":696016,"theme":696017,"keyboard":696018,"briefcase":696019,"star":696020,"star-7knmka28":696021,"protect":696022,"finance":696023,"setting":696024,"link":696025,"undo":696027,"inbox-success":696028,"home":696029,"local":696030,"laptop":696031,"view-list":696032,"lock":696033,"unlock":696034,"lightning":696035,"file-text":696036,"cooperative-handshake":696037,"navigation":696038,"view-grid-detail":696039,"help":696040,"history":696041,"logout-7knnioon":696042,"chinese":696043,"calendar":696044,"play-cycle":696045,"world":696046,"plugins":696048,"link-cloud":696049,"book":696660,"table-report":696661,"qiyeweixin":744163,"Oauth":744173,"dingding":744175,"eolink":750656,"tool":752737,"category-management":756392,"folder-code-one":757321,"link-three-8ah7lifn":757499,"download-two-8ah85008":757504,"quanjusuoxiao1":757518,"quanjufangda21":757519,"quanjusuoxiao211":757520,"quanjufangda1":757521,"wenjianshezhi":757616,"key":757650,"zidingyijiaoben":767277,"tiqubianliang":767278,"mock":775549,"tongzhishezhi":779333,"csdn":779418,"ceshibaogao":779705,"biangengtongzhi":779706,"icon-api":787702,"youjian":788577,"pushpin":802334,"announcement":804269,"collapse-text-input":804612,"zhankai":804614,"replay-music":806103,"download-web":813707,"permissions":815901,"file-editing":820089,"wallet":826687,"file-focus":854318,"pingpu-9a913n0n":855246,"zuoyoufenping-9a913n1f":855247,"shangxiafenping-9a913n1i":855248,"Paypal11":855927,"zhifubaozhifu1":855928,"weixinzhifu11":855929,"weixinzhifu":855938,"update-rotation-9and40f5":857931,"terminal":857985,"switch":861388,"zhinengrucan":876705,"biaoqian-banbenleixinzeng":884011,"book-open":885387,"morentouxiang-2":897026,"xiajia":915485,"drag":929257,"new-up":932197,"rss":949128,"yewuchangjing":970590,"newlybuild":973801,"bianji":985435,"jiekoushouquan":1002903,"interfacefenzutubiao":1021623,"yidong":1021686,"link-one":1035721,"canshugouzaoqi":1035737,"bianliang":1037074,"tars":1037815,"if":1037816,"tars-2":1037817,"yingyongguanxi":1039918,"save-one":1042170,"save":1042171};for(var _i in nm){window.__iconpark__[_i] = obj[nm[_i]]}})();"object"!=typeof globalThis&&(Object.prototype.__defineGetter__("__magic__",function(){return this}),__magic__.globalThis=__magic__,delete Object.prototype.__magic__);(()=>{"use strict";var t={816:(t,e,i)=>{var s,r,o,n;i.d(e,{Vm:()=>z,dy:()=>P,Jb:()=>x,Ld:()=>$,sY:()=>T,YP:()=>A});const l=globalThis.trustedTypes,a=l?l.createPolicy("lit-html",{createHTML:t=>t}):void 0,h=`lit$${(Math.random()+"").slice(9)}$`,c="?"+h,d=`<${c}>`,u=document,p=(t="")=>u.createComment(t),v=t=>null===t||"object"!=typeof t&&"function"!=typeof t,f=Array.isArray,y=t=>{var e;return f(t)||"function"==typeof(null===(e=t)||void 0===e?void 0:e[Symbol.iterator])},m=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,g=/-->/g,b=/>/g,S=/>|[ \n \r](?:([^\s"'>=/]+)([ \n \r]*=[ \n \r]*(?:[^ \n \r"'`<>=]|("|')|))|$)/g,w=/'/g,k=/"/g,E=/^(?:script|style|textarea)$/i,C=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),P=C(1),A=C(2),x=Symbol.for("lit-noChange"),$=Symbol.for("lit-nothing"),O=new WeakMap,T=(t,e,i)=>{var s,r;const o=null!==(s=null==i?void 0:i.renderBefore)&&void 0!==s?s:e;let n=o._$litPart$;if(void 0===n){const t=null!==(r=null==i?void 0:i.renderBefore)&&void 0!==r?r:null;o._$litPart$=n=new H(e.insertBefore(p(),t),t,void 0,i)}return n.I(t),n},R=u.createTreeWalker(u,129,null,!1),_=(t,e)=>{const i=t.length-1,s=[];let r,o=2===e?"":"",n=m;for(let e=0;e"===a[0]?(n=null!=r?r:m,c=-1):void 0===a[1]?c=-2:(c=n.lastIndex-a[2].length,l=a[1],n=void 0===a[3]?S:'"'===a[3]?k:w):n===k||n===w?n=S:n===g||n===b?n=m:(n=S,r=void 0);const p=n===S&&t[e+1].startsWith("/>")?" ":"";o+=n===m?i+d:c>=0?(s.push(l),i.slice(0,c)+"$lit$"+i.slice(c)+h+p):i+h+(-2===c?(s.push(void 0),e):p)}const l=o+(t[i]||"")+(2===e?"":"");return[void 0!==a?a.createHTML(l):l,s]};class N{constructor({strings:t,_$litType$:e},i){let s;this.parts=[];let r=0,o=0;const n=t.length-1,a=this.parts,[d,u]=_(t,e);if(this.el=N.createElement(d,i),R.currentNode=this.el.content,2===e){const t=this.el.content,e=t.firstChild;e.remove(),t.append(...e.childNodes)}for(;null!==(s=R.nextNode())&&a.length0){s.textContent=l?l.emptyScript:"";for(let i=0;i2||""!==i[0]||""!==i[1]?(this.H=Array(i.length-1).fill($),this.strings=i):this.H=$}get tagName(){return this.element.tagName}I(t,e=this,i,s){const r=this.strings;let o=!1;if(void 0===r)t=U(this,t,e,0),o=!v(t)||t!==this.H&&t!==x,o&&(this.H=t);else{const s=t;let n,l;for(t=r[0],n=0;n{i.r(e),i.d(e,{customElement:()=>s,eventOptions:()=>a,property:()=>o,query:()=>h,queryAll:()=>c,queryAssignedNodes:()=>v,queryAsync:()=>d,state:()=>n});const s=t=>e=>"function"==typeof e?((t,e)=>(window.customElements.define(t,e),e))(t,e):((t,e)=>{const{kind:i,elements:s}=e;return{kind:i,elements:s,finisher(e){window.customElements.define(t,e)}}})(t,e),r=(t,e)=>"method"===e.kind&&e.descriptor&&!("value"in e.descriptor)?{...e,finisher(i){i.createProperty(e.key,t)}}:{kind:"field",key:Symbol(),placement:"own",descriptor:{},originalKey:e.key,initializer(){"function"==typeof e.initializer&&(this[e.key]=e.initializer.call(this))},finisher(i){i.createProperty(e.key,t)}};function o(t){return(e,i)=>void 0!==i?((t,e,i)=>{e.constructor.createProperty(i,t)})(t,e,i):r(t,e)}function n(t){return o({...t,state:!0,attribute:!1})}const l=({finisher:t,descriptor:e})=>(i,s)=>{var r;if(void 0===s){const s=null!==(r=i.originalKey)&&void 0!==r?r:i.key,o=null!=e?{kind:"method",placement:"prototype",key:s,descriptor:e(i.key)}:{...i,key:s};return null!=t&&(o.finisher=function(e){t(e,s)}),o}{const r=i.constructor;void 0!==e&&Object.defineProperty(i,s,e(s)),null==t||t(r,s)}};function a(t){return l({finisher:(e,i)=>{Object.assign(e.prototype[i],t)}})}function h(t,e){return l({descriptor:i=>{const s={get(){var e;return null===(e=this.renderRoot)||void 0===e?void 0:e.querySelector(t)},enumerable:!0,configurable:!0};if(e){const e="symbol"==typeof i?Symbol():"__"+i;s.get=function(){var i;return void 0===this[e]&&(this[e]=null===(i=this.renderRoot)||void 0===i?void 0:i.querySelector(t)),this[e]}}return s}})}function c(t){return l({descriptor:e=>({get(){var e;return null===(e=this.renderRoot)||void 0===e?void 0:e.querySelectorAll(t)},enumerable:!0,configurable:!0})})}function d(t){return l({descriptor:e=>({async get(){var e;return await this.updateComplete,null===(e=this.renderRoot)||void 0===e?void 0:e.querySelector(t)},enumerable:!0,configurable:!0})})}const u=Element.prototype,p=u.msMatchesSelector||u.webkitMatchesSelector;function v(t="",e=!1,i=""){return l({descriptor:s=>({get(){var s,r;const o="slot"+(t?`[name=${t}]`:":not([name])");let n=null===(r=null===(s=this.renderRoot)||void 0===s?void 0:s.querySelector(o))||void 0===r?void 0:r.assignedNodes({flatten:e});return n&&i&&(n=n.filter((t=>t.nodeType===Node.ELEMENT_NODE&&(t.matches?t.matches(i):p.call(t,i))))),n},enumerable:!0,configurable:!0})})}},23:(t,e,i)=>{i.r(e),i.d(e,{unsafeSVG:()=>l});const s=t=>(...e)=>({_$litDirective$:t,values:e});var r=i(816);class o extends class{constructor(t){}T(t,e,i){this.Σdt=t,this.M=e,this.Σct=i}S(t,e){return this.update(t,e)}update(t,e){return this.render(...e)}}{constructor(t){if(super(t),this.vt=r.Ld,2!==t.type)throw Error(this.constructor.directiveName+"() can only be used in child bindings")}render(t){if(t===r.Ld)return this.Vt=void 0,this.vt=t;if(t===r.Jb)return t;if("string"!=typeof t)throw Error(this.constructor.directiveName+"() called with a non-string value");if(t===this.vt)return this.Vt;this.vt=t;const e=[t];return e.raw=e,this.Vt={_$litType$:this.constructor.resultType,strings:e,values:[]}}}o.directiveName="unsafeHTML",o.resultType=1,s(o);class n extends o{}n.directiveName="unsafeSVG",n.resultType=2;const l=s(n)},249:(t,e,i)=>{i.r(e),i.d(e,{CSSResult:()=>n,LitElement:()=>x,ReactiveElement:()=>b,UpdatingElement:()=>A,_Σ:()=>s.Vm,_Φ:()=>$,adoptStyles:()=>c,css:()=>h,defaultConverter:()=>y,getCompatibleStyle:()=>d,html:()=>s.dy,noChange:()=>s.Jb,notEqual:()=>m,nothing:()=>s.Ld,render:()=>s.sY,supportsAdoptingStyleSheets:()=>r,svg:()=>s.YP,unsafeCSS:()=>l});var s=i(816);const r=window.ShadowRoot&&(void 0===window.ShadyCSS||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,o=Symbol();class n{constructor(t,e){if(e!==o)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t}get styleSheet(){return r&&void 0===this.t&&(this.t=new CSSStyleSheet,this.t.replaceSync(this.cssText)),this.t}toString(){return this.cssText}}const l=t=>new n(t+"",o),a=new Map,h=(t,...e)=>{const i=e.reduce(((e,i,s)=>e+(t=>{if(t instanceof n)return t.cssText;if("number"==typeof t)return t;throw Error(`Value passed to 'css' function must be a 'css' function result: ${t}. Use 'unsafeCSS' to pass non-literal values, but\n take care to ensure page security.`)})(i)+t[s+1]),t[0]);let s=a.get(i);return void 0===s&&a.set(i,s=new n(i,o)),s},c=(t,e)=>{r?t.adoptedStyleSheets=e.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):e.forEach((e=>{const i=document.createElement("style");i.textContent=e.cssText,t.appendChild(i)}))},d=r?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return l(e)})(t):t;var u,p,v,f;const y={toAttribute(t,e){switch(e){case Boolean:t=t?"":null;break;case Object:case Array:t=null==t?t:JSON.stringify(t)}return t},fromAttribute(t,e){let i=t;switch(e){case Boolean:i=null!==t;break;case Number:i=null===t?null:Number(t);break;case Object:case Array:try{i=JSON.parse(t)}catch(t){i=null}}return i}},m=(t,e)=>e!==t&&(e==e||t==t),g={attribute:!0,type:String,converter:y,reflect:!1,hasChanged:m};class b extends HTMLElement{constructor(){super(),this.Πi=new Map,this.Πo=void 0,this.Πl=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this.Πh=null,this.u()}static addInitializer(t){var e;null!==(e=this.v)&&void 0!==e||(this.v=[]),this.v.push(t)}static get observedAttributes(){this.finalize();const t=[];return this.elementProperties.forEach(((e,i)=>{const s=this.Πp(i,e);void 0!==s&&(this.Πm.set(s,i),t.push(s))})),t}static createProperty(t,e=g){if(e.state&&(e.attribute=!1),this.finalize(),this.elementProperties.set(t,e),!e.noAccessor&&!this.prototype.hasOwnProperty(t)){const i="symbol"==typeof t?Symbol():"__"+t,s=this.getPropertyDescriptor(t,i,e);void 0!==s&&Object.defineProperty(this.prototype,t,s)}}static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(s){const r=this[t];this[e]=s,this.requestUpdate(t,r,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||g}static finalize(){if(this.hasOwnProperty("finalized"))return!1;this.finalized=!0;const t=Object.getPrototypeOf(this);if(t.finalize(),this.elementProperties=new Map(t.elementProperties),this.Πm=new Map,this.hasOwnProperty("properties")){const t=this.properties,e=[...Object.getOwnPropertyNames(t),...Object.getOwnPropertySymbols(t)];for(const i of e)this.createProperty(i,t[i])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(t){const e=[];if(Array.isArray(t)){const i=new Set(t.flat(1/0).reverse());for(const t of i)e.unshift(d(t))}else void 0!==t&&e.push(d(t));return e}static Πp(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}u(){var t;this.Πg=new Promise((t=>this.enableUpdating=t)),this.L=new Map,this.Π_(),this.requestUpdate(),null===(t=this.constructor.v)||void 0===t||t.forEach((t=>t(this)))}addController(t){var e,i;(null!==(e=this.ΠU)&&void 0!==e?e:this.ΠU=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(i=t.hostConnected)||void 0===i||i.call(t))}removeController(t){var e;null===(e=this.ΠU)||void 0===e||e.splice(this.ΠU.indexOf(t)>>>0,1)}Π_(){this.constructor.elementProperties.forEach(((t,e)=>{this.hasOwnProperty(e)&&(this.Πi.set(e,this[e]),delete this[e])}))}createRenderRoot(){var t;const e=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return c(e,this.constructor.elementStyles),e}connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this.ΠU)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostConnected)||void 0===e?void 0:e.call(t)})),this.Πl&&(this.Πl(),this.Πo=this.Πl=void 0)}enableUpdating(t){}disconnectedCallback(){var t;null===(t=this.ΠU)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostDisconnected)||void 0===e?void 0:e.call(t)})),this.Πo=new Promise((t=>this.Πl=t))}attributeChangedCallback(t,e,i){this.K(t,i)}Πj(t,e,i=g){var s,r;const o=this.constructor.Πp(t,i);if(void 0!==o&&!0===i.reflect){const n=(null!==(r=null===(s=i.converter)||void 0===s?void 0:s.toAttribute)&&void 0!==r?r:y.toAttribute)(e,i.type);this.Πh=t,null==n?this.removeAttribute(o):this.setAttribute(o,n),this.Πh=null}}K(t,e){var i,s,r;const o=this.constructor,n=o.Πm.get(t);if(void 0!==n&&this.Πh!==n){const t=o.getPropertyOptions(n),l=t.converter,a=null!==(r=null!==(s=null===(i=l)||void 0===i?void 0:i.fromAttribute)&&void 0!==s?s:"function"==typeof l?l:null)&&void 0!==r?r:y.fromAttribute;this.Πh=n,this[n]=a(e,t.type),this.Πh=null}}requestUpdate(t,e,i){let s=!0;void 0!==t&&(((i=i||this.constructor.getPropertyOptions(t)).hasChanged||m)(this[t],e)?(this.L.has(t)||this.L.set(t,e),!0===i.reflect&&this.Πh!==t&&(void 0===this.Πk&&(this.Πk=new Map),this.Πk.set(t,i))):s=!1),!this.isUpdatePending&&s&&(this.Πg=this.Πq())}async Πq(){this.isUpdatePending=!0;try{for(await this.Πg;this.Πo;)await this.Πo}catch(t){Promise.reject(t)}const t=this.performUpdate();return null!=t&&await t,!this.isUpdatePending}performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,this.Πi&&(this.Πi.forEach(((t,e)=>this[e]=t)),this.Πi=void 0);let e=!1;const i=this.L;try{e=this.shouldUpdate(i),e?(this.willUpdate(i),null===(t=this.ΠU)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostUpdate)||void 0===e?void 0:e.call(t)})),this.update(i)):this.Π$()}catch(t){throw e=!1,this.Π$(),t}e&&this.E(i)}willUpdate(t){}E(t){var e;null===(e=this.ΠU)||void 0===e||e.forEach((t=>{var e;return null===(e=t.hostUpdated)||void 0===e?void 0:e.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}Π$(){this.L=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this.Πg}shouldUpdate(t){return!0}update(t){void 0!==this.Πk&&(this.Πk.forEach(((t,e)=>this.Πj(e,this[e],t))),this.Πk=void 0),this.Π$()}updated(t){}firstUpdated(t){}}var S,w,k,E,C,P;b.finalized=!0,b.shadowRootOptions={mode:"open"},null===(p=(u=globalThis).reactiveElementPlatformSupport)||void 0===p||p.call(u,{ReactiveElement:b}),(null!==(v=(f=globalThis).reactiveElementVersions)&&void 0!==v?v:f.reactiveElementVersions=[]).push("1.0.0-rc.1");const A=b;(null!==(S=(P=globalThis).litElementVersions)&&void 0!==S?S:P.litElementVersions=[]).push("3.0.0-rc.1");class x extends b{constructor(){super(...arguments),this.renderOptions={host:this},this.Φt=void 0}createRenderRoot(){var t,e;const i=super.createRenderRoot();return null!==(t=(e=this.renderOptions).renderBefore)&&void 0!==t||(e.renderBefore=i.firstChild),i}update(t){const e=this.render();super.update(t),this.Φt=(0,s.sY)(e,this.renderRoot,this.renderOptions)}connectedCallback(){var t;super.connectedCallback(),null===(t=this.Φt)||void 0===t||t.setConnected(!0)}disconnectedCallback(){var t;super.disconnectedCallback(),null===(t=this.Φt)||void 0===t||t.setConnected(!1)}render(){return s.Jb}}x.finalized=!0,x._$litElement$=!0,null===(k=(w=globalThis).litElementHydrateSupport)||void 0===k||k.call(w,{LitElement:x}),null===(C=(E=globalThis).litElementPlatformSupport)||void 0===C||C.call(E,{LitElement:x});const $={K:(t,e,i)=>{t.K(e,i)},L:t=>t.L}},409:function(t,e,i){var s=this&&this.__decorate||function(t,e,i,s){var r,o=arguments.length,n=o<3?e:null===s?s=Object.getOwnPropertyDescriptor(e,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,i,s);else for(var l=t.length-1;l>=0;l--)(r=t[l])&&(n=(o<3?r(n):o>3?r(e,i,n):r(e,i))||n);return o>3&&n&&Object.defineProperty(e,i,n),n};Object.defineProperty(e,"__esModule",{value:!0}),e.IconparkIconElement=void 0;const r=i(249),o=i(26),n=i(23),l={color:1,fill:1,stroke:1},a={STROKE:{trackAttr:"data-follow-stroke",rawAttr:"stroke"},FILL:{trackAttr:"data-follow-fill",rawAttr:"fill"}};class h extends r.LitElement{constructor(){super(...arguments),this.name="",this.identifyer="",this.size="1em"}get _width(){return this.width||this.size}get _height(){return this.height||this.size}get _stroke(){return this.stroke||this.color}get _fill(){return this.fill||this.color}get SVGConfig(){return(window.__iconpark__||{})[this.identifyer]||(window.__iconpark__||{})[this.name]||{viewBox:"0 0 0 0",content:""}}connectedCallback(){super.connectedCallback(),setTimeout((()=>{this.monkeyPatch("STROKE",!0),this.monkeyPatch("FILL",!0)}))}monkeyPatch(t,e){switch(t){case"STROKE":this.updateDOMByHand(this.strokeAppliedNodes,"STROKE",this._stroke,!!e);break;case"FILL":this.updateDOMByHand(this.fillAppliedNodes,"FILL",this._fill,!!e)}}updateDOMByHand(t,e,i,s){!i&&s||t&&t.forEach((t=>{i&&i===t.getAttribute(a[e].rawAttr)||t.setAttribute(a[e].rawAttr,i||t.getAttribute(a[e].trackAttr))}))}attributeChangedCallback(t,e,i){super.attributeChangedCallback(t,e,i),"name"===t||"identifyer"===t?setTimeout((()=>{this.monkeyPatch("STROKE"),this.monkeyPatch("FILL")})):l[t]&&(this.monkeyPatch("STROKE"),this.monkeyPatch("FILL"))}render(){return r.svg`${n.unsafeSVG(this.SVGConfig.content)}`}}h.styles=r.css`:host {display: inline-flex; align-items: center; justify-content: center;} :host([spin]) svg {animation: iconpark-spin 1s infinite linear;} :host([spin][rtl]) svg {animation: iconpark-spin-rtl 1s infinite linear;} :host([rtl]) svg {transform: scaleX(-1);} @keyframes iconpark-spin {0% { -webkit-transform: rotate(0); transform: rotate(0);} 100% {-webkit-transform: rotate(360deg); transform: rotate(360deg);}} @keyframes iconpark-spin-rtl {0% {-webkit-transform: scaleX(-1) rotate(0); transform: scaleX(-1) rotate(0);} 100% {-webkit-transform: scaleX(-1) rotate(360deg); transform: scaleX(-1) rotate(360deg);}}`,s([o.property({reflect:!0})],h.prototype,"name",void 0),s([o.property({reflect:!0,attribute:"icon-id"})],h.prototype,"identifyer",void 0),s([o.property({reflect:!0})],h.prototype,"color",void 0),s([o.property({reflect:!0})],h.prototype,"stroke",void 0),s([o.property({reflect:!0})],h.prototype,"fill",void 0),s([o.property({reflect:!0})],h.prototype,"size",void 0),s([o.property({reflect:!0})],h.prototype,"width",void 0),s([o.property({reflect:!0})],h.prototype,"height",void 0),s([o.queryAll(`[${a.STROKE.trackAttr}]`)],h.prototype,"strokeAppliedNodes",void 0),s([o.queryAll(`[${a.FILL.trackAttr}]`)],h.prototype,"fillAppliedNodes",void 0),e.IconparkIconElement=h,customElements.get("iconpark-icon")||customElements.define("iconpark-icon",h)}},e={};function i(s){var r=e[s];if(void 0!==r)return r.exports;var o=e[s]={exports:{}};return t[s].call(o.exports,o,o.exports,i),o.exports}i.d=(t,e)=>{for(var s in e)i.o(e,s)&&!i.o(t,s)&&Object.defineProperty(t,s,{enumerable:!0,get:e[s]})},i.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e),i.r=t=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},i(409)})(); \ No newline at end of file diff --git a/frontend/packages/core/public/vite.svg b/frontend/packages/core/public/vite.svg new file mode 100644 index 00000000..e7b8dfb1 --- /dev/null +++ b/frontend/packages/core/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/packages/core/scripts/moveTinymce.js b/frontend/packages/core/scripts/moveTinymce.js new file mode 100644 index 00000000..1c765ef9 --- /dev/null +++ b/frontend/packages/core/scripts/moveTinymce.js @@ -0,0 +1,13 @@ +import fse from 'fs-extra'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const tinymcePath = path.resolve(__dirname, '../node_modules/tinymce'); +const destPath = path.resolve(__dirname, '../public/tinymce'); + +fse.removeSync(destPath); // 删除旧目录 + +// 使用 fs-extra 的 copySync 方法来复制目录 +fse.copySync(tinymcePath, destPath, { dereference: true }); \ No newline at end of file diff --git a/frontend/packages/core/src/App.css b/frontend/packages/core/src/App.css new file mode 100644 index 00000000..583b5ad5 --- /dev/null +++ b/frontend/packages/core/src/App.css @@ -0,0 +1,215 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + + +#root { + width: 100vw; + height:100vh; +} + +:global.ant-tree-node-content-wrapper{ + overflow: hidden; +} +.tree-title-hover{ + display: flex; + justify-content: space-between; + align-items:center; + .tree-title-span{ + text-overflow: ellipsis; + } + .tree-title-more{ + display: none; + } + + &:hover .tree-title-more{ + display: flex; + height:22px; + width:22px; + } +} + + +.ant-layout-content.apipark-layout-layout-content{ + border-radius:10px 0 0 0 ; + overflow:hidden; + background-color:'transparent' +} + +/* .ant-layout-sider-children{ + ul.ant-menu.ant-menu-root.ant-menu-inline > li:not(.ant-menu-item-selected) { + border-radius: 10px; + background-color: rgba(255,255,255,0.1); + border: 1px solid rgba(255,255,255,0.15); + } + .ant-menu-item { + height:40px; + margin-block:10px; + } +} */ + +.apipark-layout-global-header-collapsed-button{ + color:hsl(0, 0%, 100%); +} + +.apipark-layout-top-nav-header-main{ + display: flex; + align-items: center; +} +.apipark-layout-top-nav-header-menu { + height:50px; + line-height:50px; + + .ant-menu-item.apipark-layout-base-menu-horizontal-menu-item.ant-menu-item-selected::after{ + border-bottom:2px solid #fff !important; + } + .ant-menu-item.apipark-layout-base-menu-horizontal-menu-item.ant-menu-item-active:not(.ant-menu-item-selected)::after{ + border-bottom:2px solid transparent !important; + } +} +.apipark-layout-base-menu-inline-group .ant-menu-item-group-title{ + color:rgb(255 255 255 / 70%) !important; +} +.avatar-dom > div{ + display: flex; + flex-direction: row-reverse; + align-items: center; + gap:8px; +} + +.apipark-layout-layout{ + + .apipark-layout-layout-bg-list{ + background:#17163E; + } + .ant-layout-header.apipark-layout-layout-header{ + backdrop-filter: unset !important; + height:var(--layout-header-height); + line-height: var(--layout-header-height); + background-color: transparent; + + li.apipark-layout-base-menu-horizontal-menu-item{ + color:rgb(255 255 255 / 70%) !important; + + &.ant-menu-item-selected{ + color:#fff !important; + } + &.ant-menu-item-active{ + color:#fff !important; + } + } + } + .ant-layout-sider.apipark-layout-sider{ + height:calc(100vh - var(--layout-header-height)) !important; + inset-block-start: var(--layout-header-height); + + .ant-menu { + .ant-menu-item-group-title{ + font-size:12px; + padding:12px 16px; + } + + .ant-menu-item{ + margin-block:0 !important; + } + } + + .apipark-layout-sider-collapsed-button{ + display: none; + } + + ul.ant-menu.ant-menu-root.ant-menu-inline, + ul.ant-menu.ant-menu-root.ant-menu-vertical{ + > li { + color:rgb(255 255 255 / 70%) !important; + /* border-radius: 10px; + background-color: rgba(255,255,255,0.1) !important; + border: 1px solid rgba(255,255,255,0.15); */ + } + > li.ant-menu-item-active { + color:#fff !important; + } + > li.ant-menu-item-selected { + background-color: #fff !important; + border: 1px solid #fff !important; + color:#333 !important; + } + } + ul.apipark-layout-sider-menu .ant-menu-item-group-list{ + > li { + color:rgb(255 255 255 / 70%) !important; + } + > li:active{ + background-color: transparent; + } + /* > li.ant-menu-item-active { + color:#fff !important; + } */ + > li.ant-menu-item-selected { + background-color: #fff !important; + border: 1px solid #fff !important; + color:#333 !important; + } + } + .ant-menu-item { + height:40px; + margin-block:10px; + } + } + .apipark-layout-drawer-sider{ + background:#17163E; + padding-top:20px; + .ant-layout-sider.apipark-layout-sider{ + height: 100% !important; + inset-block: 20px; + } + } + + .apipark-layout-layout-container{ + + >.ant-layout-header{ + height:var(--layout-header-height) !important; + line-height:var(--layout-header-height) !important; + } + + >.apipark-layout-layout-content.apipark-layout-layout-has-header{ + padding-block:0px; + padding-inline:0px; + background-color: #fff !important; + } + } + .ant-pro-global-header-header-actions-avatar > div{ + color:#fff !important; + } + + .ant-menu-item-divider.apipark-layout-base-menu-inline-divider{ + border-color: rgb(255 255 255 / 15%) !important; + } +} + +.tox-tinymce{ + border:none !important; +} + +a{ + transition:none !important; +} + +.ant-result ant-result-error{ + background-color: #fff !important; +} + +.ant-tabs-tab-btn{ + display: flex; + align-items:center; + .ant-tabs-tab-icon{ + display: inline-flex; + align-items:center; + } +} + +.eo_page_list .ant-pro-table{ + overflow: hidden; + border-radius: 10px; + border:1px solid var(--table-border-color) !important; +} \ No newline at end of file diff --git a/frontend/packages/core/src/App.tsx b/frontend/packages/core/src/App.tsx new file mode 100644 index 00000000..9353440c --- /dev/null +++ b/frontend/packages/core/src/App.tsx @@ -0,0 +1,148 @@ + +import './App.css' +import { ConfigProvider } from 'antd'; +import RenderRoutes from '@core/components/aoplatform/RenderRoutes'; +import {BreadcrumbProvider} from "@common/contexts/BreadcrumbContext.tsx"; +import { StyleProvider } from '@ant-design/cssinjs'; +import zhCN from 'antd/locale/zh_CN'; +import useInitializeMonaco from "@common/hooks/useInitializeMonaco"; + +const antdComponentThemeToken = { + token: { + // Seed Token,影响范围大 + colorPrimary: '#3D46F2', + colorLink:'#3D46F2', + colorBorder:'#ededed', + colorText:'#333', + borderRadius: 4, + // 派生变量,影响范围小 + colorBgContainer: '#fff', + colorPrimaryBg:'#EBEEF2', + colorTextQuaternary:'#BBB', + colorTextTertiary:'#999' + }, + components:{ + // 派生变量,影响范围小 + Input:{ + activeShadow:'none' + }, + Select:{ + activeShadow:'none' + }, + Checkbox:{ + activeShadow:'none' + }, + Cascader:{ + activeShadow:'none', + optionSelectedBg:'#EBEEF2', + optionHoverBg:'#EBEEF2' + }, + Layout: { + bodyBg: '#fff', + headerBg: '#fff', + headerColor: '#333', + headerHeight: 50, + headerPadding: '10 20px', + lightSiderBg: '#fff', + siderBg: '#fff', + }, + Breadcrumb:{ + itemColor:'#666', + linkColor:'#666', + lastItemColor:'#333', + }, + Table:{ + headerBorderRadius:0, + headerSplitColor:'#ededed', + borderColor:'#ededed', + cellPaddingBlockMD:'15px', + cellPaddingInlineMD:'12px', + cellPaddingBlockSM:'8px', + cellPaddingInlineSM:'12px', + headerFilterHoverBg:'#EBEEF2', + headerSortActiveBg:'#F7F8FA', + headerSortHoverBg:'#F7F8FA', + fixedHeaderSortActiveBg:'#F7F8FA', + headerBg:'#FAFAFA', + rowHoverBg:'#EBEEF2' + + }, + Segmented:{ + itemColor:'#333', + itemSelectedColor:'#333', + trackBg:'#f7f8fa', + trackPadding:0, + // itemHoverColor:'#EBEEF2', + itemActiveBg:'#EBEEF2', + itemHoverBg:'#EBEEF2', + itemSelectedBg:'#EBEEF2', + }, + Tree:{ + // titleHeight:30, + // fontSize:12, + directoryNodeSelectedBg:'#EBEEF2', + directoryNodeSelectedColor:'#333', + nodeSelectedBg:'#EBEEF2', + nodeHoverBg:'#EBEEF2' + }, + Collapse:{ + headerBg:'#f7f8fa', + headerPadding:"12px", + contentPadding:"0 10px 12px 10px" + }, + Button:{ + // paddingInline:8, + dangerShadow:'none', + defaultShadow:'none', + primaryShadow:'none' + }, + Tabs:{ + cardBg:'#EBEEF2', + cardHeight:42, + horizontalItemGutter:8, + horizontalItemPaddingSM:'12px 8px 8px 8px', + horizontalItemPadding:'12px 8px 8px 8px', + }, + Menu:{ + // itemBg:'#F7F8FA', + // subMenuItemBg:'#F7F8FA', + // itemMarginBlock:0, + // activeBarBorderWidth:0, + // itemSelectedColor:'#333', + // itemSelectedBg:'#EBEEF2', + // itemHoverBg:'#EBEEF2' + }, + List:{ + itemPadding:'8px 0' + }, + Form:{ + itemMarginBottom:10, + + }, + Alert:{ + defaultPadding:'12px 16px' + }, + Tag:{ + defaultBg:"#f7f8fa" + }, + } +} + +function App() { + useInitializeMonaco() + + return ( + + + + + + + + ); +} + +export default App diff --git a/frontend/packages/core/src/components/aoplatform/RenderRoutes.tsx b/frontend/packages/core/src/components/aoplatform/RenderRoutes.tsx new file mode 100644 index 00000000..7f014904 --- /dev/null +++ b/frontend/packages/core/src/components/aoplatform/RenderRoutes.tsx @@ -0,0 +1,441 @@ + +import { BrowserRouter as Router, Routes, Route, Navigate, Outlet } from 'react-router-dom'; +import Login from "@core/pages/Login.tsx" +import BasicLayout from '@common/components/aoplatform/BasicLayout'; +import {createElement, ReactElement,ReactNode,Suspense} from 'react'; +import { v4 as uuidv4 } from 'uuid' +import {App, Skeleton} from "antd"; +import ApprovalPage from "@core/pages/approval/ApprovalPage.tsx"; +import {SystemProvider} from "@core/contexts/SystemContext.tsx"; +import {useGlobalContext} from "@common/contexts/GlobalStateContext.tsx"; +import {FC,lazy} from 'react'; +import { TeamProvider } from '@core/contexts/TeamContext.tsx'; +import SystemOutlet from '@core/pages/system/SystemOutlet.tsx'; +import { DashboardProvider } from '@core/contexts/DashboardContext.tsx'; +import { PartitionProvider } from '@core/contexts/PartitionContext.tsx'; +import { TenantManagementProvider } from '@market/contexts/TenantManagementContext.tsx'; + +type RouteConfig = { + path:string + component?:ReactElement + children?:(RouteConfig|false)[] + key:string + provider?:FC<{ children: ReactNode; }> + lazy?:unknown +} +const APP_MODE = import.meta.env.VITE_APP_MODE; + +export type RouterParams = { + teamId:string + apiId:string + serviceId:string + clusterId:string; + memberGroupId:string + userGroupId:string + pluginName:string + moduleId:string + accessType:'project'|'team'|'service' + categoryId:string + tagId:string + dashboardType:string + dashboardDetailId:string + topologyId:string + appId:string + roleType:string + roleId:string +} + +const PUBLIC_ROUTES:RouteConfig[] = [ + { + path:'/', + component:, + key: uuidv4(), + }, + { + path:'/login', + component:, + key: uuidv4() + }, + { + path:'/', + component:, + key: uuidv4(), + children:[ + { + path:'approval/*', + component:, + key:uuidv4() + }, + { + path:'team', + component:, + key: uuidv4(), + provider: TeamProvider, + children:[ + { + path:'', + key: uuidv4(), + component: + }, + { + path:'list', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamList.tsx')) + }, + { + path:'inside/:teamId', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamInsidePage.tsx')), + key: uuidv4(), + children:[ + { + path:'member', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamInsideMember.tsx')), + }, + { + path:'setting', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/team/TeamConfig.tsx')), + }, + ] + } + ] + }, + { + path:'service', + component:, + key: uuidv4(), + provider: SystemProvider, + children:[ + { + path:'', + key:uuidv4(), + component: + }, + { + path:'list', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemList.tsx')), + }, + { + path:'list/:teamId', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemList.tsx')), + }, + { + path:':teamId', + component:, + key: uuidv4(), + children:[ + { + path:'inside/:serviceId', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemInsidePage.tsx')), + children:[ + { + path:'api', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/api/SystemInsideApiList.tsx')), + }, + { + path:'upstream', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/upstream/SystemInsideUpstreamContent.tsx')), + }, + { + path:'document', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemInsideDocument.tsx')), + }, + { + path:'subscriber', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemInsideSubscriber.tsx')), + children:[ + + ] + }, + { + path:'approval', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/approval/SystemInsideApproval.tsx')), + children:[ + { + path:'', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/approval/SystemInsideApprovalList.tsx')), + }, + { + path:'*', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/approval/SystemInsideApprovalList.tsx')), + } + ] + }, + { + path:'topology', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemTopology.tsx')), + key: uuidv4(), + children:[ + ] + }, + { + path:'publish', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/publish/SystemInsidePublish.tsx')), + children:[ + { + path:'', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/publish/SystemInsidePublishList.tsx')), + }, + { + path:'*', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/publish/SystemInsidePublishList.tsx')), + } + ] + }, + { + path:'setting', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/system/SystemConfig.tsx')), + children:[ + + ] + }, + ] + } + ] + } + ] + }, + { + path:'cluster', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/partitions/PartitionInsideCluster.tsx')), + }, + { + path:'cert', + key: uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/partitions/PartitionInsideCert.tsx')), + }, + { + path:'serviceHub', + component:, + key:uuidv4(), + children:[ + { + path:'', + key: uuidv4(), + component: + }, + { + path:'list', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/ServiceHubList.tsx')), + }, + { + path:'detail/:serviceId', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/ServiceHubDetail.tsx')), + }] + }, + { + path:'servicecategories', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/serviceCategory/ServiceCategory.tsx')), + key:uuidv4(), + }, + { + path:'tenantManagement', + component:, + provider:TenantManagementProvider, + key:uuidv4(), + children:[ + { + path:'', + key:uuidv4(), + component: + }, + { + path:':teamId/inside/:appId', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementInsidePage.tsx')), + children:[ + { + path:'service', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementInsideService.tsx')), + }, + { + path:'authorization', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementInsideAuth.tsx')), + }, + { + path:'setting', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ManagementAppSetting.tsx')), + }, + ] + }, + { + path:'list', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ServiceHubManagement.tsx')), + }, + { + path:'list/:teamId', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@market/pages/serviceHub/management/ServiceHubManagement.tsx')), + }, + ] + }, + { + path:'member', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/member/MemberPage.tsx')), + children:[ + { + path:'', + key:uuidv4(), + component: + }, + { + path:'list', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/member/MemberList.tsx')), + }, + { + path:'list/:memberGroupId', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/member/MemberList.tsx')), + } + ] + }, + { + path:'role', + key:uuidv4(), + component:, + children:[ + { + path: '', + key: uuidv4(), + component: + }, + { + path:'list', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/role/RoleList.tsx')), + },{ + path:':roleType/config/:roleId', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/role/RoleConfig.tsx')), + },{ + path:':roleType/config', + key:uuidv4(), + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/role/RoleConfig.tsx')), + } + ] + }, + { + path:'logretrieval', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/logRetrieval/LogRetrieval.tsx')), + key:uuidv4(), + }, + { + path:'auditlog', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/auditLog/AuditLog.tsx')), + key:uuidv4(), + }, + { + path:'assets', + component:

设计中

, + key:uuidv4() + }, + { + path:'template/:moduleId', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@common/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx')), + key:uuidv4() + }, + { + path:'logsettings/*', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/logsettings/LogSettings.tsx')), + key: uuidv4(), + children:[{ + path:'template/:moduleId', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@common/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx')), + key:uuidv4() + }] + + }, + APP_MODE ==='pro' && { + path:'resourcesettings/*', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/resourcesettings/ResourceSettings.tsx')), + key: uuidv4(), + children:[{ + path:'template/:moduleId', + lazy:lazy(() => import(/* webpackChunkName: "[request]" */ '@common/components/aoplatform/intelligent-plugin/IntelligentPluginList.tsx')), + key:uuidv4() + }] + + } + ] + }, +] + +const RenderRoutes = ()=> { + return ( + + + + {generateRoutes(PUBLIC_ROUTES)} + + + + ) +} + +const generateRoutes = (routerConfig: RouteConfig[]) => { + return routerConfig?.map((route: RouteConfig) => { + let routeElement; + if (route.lazy) { + const LazyComponent = route.lazy as React.ExoticComponent; + + routeElement = ( + }> + {route.provider ? ( + createElement(route.provider, {}, ) + ) : ( + + )} + + ); + } else { + routeElement = route.provider ? ( + createElement(route.provider, {}, route.component) + ) : ( + route.component + ); + } + + return ( + + {route.children && generateRoutes(route.children as RouteConfig[])} + + ); + } + ) +} + +// 保护的路由组件 +function ProtectedRoute() { + const {state} = useGlobalContext() + return state.isAuthenticated? : ; + } + +export default RenderRoutes \ No newline at end of file diff --git a/frontend/packages/core/src/const/member/const.tsx b/frontend/packages/core/src/const/member/const.tsx new file mode 100644 index 00000000..db321600 --- /dev/null +++ b/frontend/packages/core/src/const/member/const.tsx @@ -0,0 +1,54 @@ + +import { ProColumns } from "@ant-design/pro-components"; +import { MemberTableListItem } from "./type"; + + +export const MEMBER_TABLE_COLUMNS: ProColumns[] = [ + { + title: '用户名', + dataIndex: 'name', + copyable: true, + ellipsis:true, + width:160, + fixed:'left', + sorter: (a,b)=> { + return a.name.localeCompare(b.name) + }, + }, + { + title: '邮箱', + dataIndex: 'email', + copyable: true, + ellipsis:true, + }, + { + title: '部门', + dataIndex: 'department', + copyable: true, + ellipsis:true, + filterMode:'tree', + renderText:(_,entity:MemberTableListItem)=>(entity.department?.map(x=>x.name).join(',') || '-'), + filters: [], + onFilter: true, + // valueType: 'select', + filterSearch: true, + }, + { + title: '角色', + dataIndex: 'roles', + copyable: true, + ellipsis:true, + width:200 + }, + { + title:'状态', + dataIndex:'enable', + valueType: 'select', + filters: true, + onFilter: true, + valueEnum:new Map([ + [true,启用], + [false,禁用], + ]) + } +]; diff --git a/frontend/packages/core/src/const/member/type.ts b/frontend/packages/core/src/const/member/type.ts new file mode 100644 index 00000000..2acc8d34 --- /dev/null +++ b/frontend/packages/core/src/const/member/type.ts @@ -0,0 +1,48 @@ + +import { EntityItem } from "@common/const/type" + +export type DepartmentListItem = { + id:string + name:string + number?:string + children:DepartmentListItem[] + departmentIds?:string[] + key?:string +} + +export type MemberTableListItem = { + id:string; + name: string; + email:string; + department:Array; + userGroup:Array; + enable:boolean + departmentId:string + roles:EntityItem[] +}; + +export type AddToDepartmentProps = { + selectedUserIds:string[] +} + +export type AddToDepartmentHandle = { + save:()=>Promise +} + +export type MemberDropdownModalFieldType = { + id?:string + name:string + parent?:string + email?:string + departmentIds?:string[] +}; + +export type MemberDropdownModalProps = { + type:'addDep'|'addChild'|'addMember'|'editMember'|'rename' + entity?:(MemberTableListItem & {departmentIds:string[]}) | ({id?:string, departmentIds?:string[],name?:string}) + selectedMemberGroupId?:string +} + +export type MemberDropdownModalHandle = { + save:()=>Promise +} diff --git a/frontend/packages/core/src/const/partitions/const.tsx b/frontend/packages/core/src/const/partitions/const.tsx new file mode 100644 index 00000000..dae636db --- /dev/null +++ b/frontend/packages/core/src/const/partitions/const.tsx @@ -0,0 +1,214 @@ +import { ProColumns } from "@ant-design/pro-components"; +import { PartitionCertTableListItem, PartitionClusterNodeModalTableListItem, PartitionClusterNodeTableListItem, PartitionClusterTableListItem, PartitionTableListItem } from "./types"; +import { ColumnType } from "antd/es/table"; +import CopyAddrList from "@common/components/aoplatform/CopyAddrList"; + + +export const PARTITION_CERT_TABLE_COLUMNS: ProColumns[] = [ + { + title: '证书', + dataIndex: 'name', + copyable: true, + ellipsis:true, + width:160, + fixed:'left', + sorter: (a,b)=> { + return a.name.localeCompare(b.name) + }, + }, + { + title: '绑定域名', + dataIndex: 'domains', + renderText:(_,entity) =>( + entity.domains.join(',') + ), + copyable: true, + ellipsis:true + }, + { + title: '证书有效期', + ellipsis: true, + dataIndex: 'notAfter', + copyable: true, + width:320, + renderText: (value:string,entity:PartitionCertTableListItem) => { + return `${entity.notBefore} - ${entity.notAfter}` + }, + }, + { + title: '更新者', + dataIndex: ['updater','name'], + ellipsis: true, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true + }, + { + title: '更新时间', + key: 'updateTime', + dataIndex: 'updateTime', + ellipsis:true, + width:182, + sorter: (a,b)=> { + return a.updateTime.localeCompare(b.updateTime) + }, + }, +]; + +export const PARTITION_CLUSTER_TABLE_COLUMNS : ProColumns[] = [ + { + title: '集群名称', + dataIndex: 'name', + copyable: true, + ellipsis:true, + width:160, + fixed:'left', + sorter: (a,b)=> { + return a.name.localeCompare(b.name) + }, + }, + { + title: '集群 ID', + dataIndex: 'id', + width: 140, + copyable: true, + ellipsis:true + }, + { + title: '状态', + dataIndex: 'status', + ellipsis:true, + valueType: 'select', + filters: true, + onFilter: true, + valueEnum: new Map([ + [0, 异常], + [1,正常], + ]) + }, + { + title: '描述', + dataIndex: 'description', + copyable: true, + ellipsis:true + } +]; + + +export const PARTITION_CLUSTER_NODE_COLUMNS: ProColumns[] = [ + { + title: '节点名称', + dataIndex: 'name', + copyable: true, + ellipsis:true, + fixed:'left', + sorter: (a,b)=> { + return a.name.localeCompare(b.name) + }, + }, + { + title: '管理地址', + dataIndex: 'managerAddress', + ellipsis:true, + width:200, + render:(_,entity)=>() + }, + { + title: '服务地址', + dataIndex: 'serviceAddress', + ellipsis:true, + width:230, + render:(_,entity)=>() + }, + { + title: '集群同步地址', + dataIndex: 'peerAddress', + ellipsis:true, + width:230, + render:(_,entity)=>() + }, + { + title: '状态', + dataIndex: 'status', + ellipsis:true, + width:86, + valueType: 'select', + filters: true, + onFilter: true, + valueEnum: new Map([ + [0, 异常], + [1,正常], + ]) + }, +]; + +export const NODE_MODAL_COLUMNS:ColumnType[] = [ + {title:'名称', dataIndex:'name',width:200, + ellipsis:true, + fixed:'left'}, + {title:'管理地址', dataIndex:'managerAddress',width:240,ellipsis:true,render:(_,entity)=>()}, + {title:'服务地址', dataIndex:'serviceAddress',width:240,ellipsis:true,render:(_,entity)=>()}, + {title:'状态', dataIndex:'status', + render:(text)=>( + {ClusterStatusEnum[text]} + )} +] + +export const PARTITION_LIST_COLUMNS: ProColumns[] = [ + { + title: '环境名称', + dataIndex: 'name', + copyable: true, + ellipsis:true, + fixed:'left', + sorter: (a,b)=> { + return a.name.localeCompare(b.name) + }, + }, + { + title: 'ID', + dataIndex: 'id', + copyable: true, + ellipsis:true, + width:140, + }, + // { + // title: '集群数量', + // dataIndex: 'clusterNum', + // sorter: (a,b)=> { + // return a.clusterNum - b.clusterNum + // }, + // }, + { + title: '更新者', + dataIndex: ['updater','name'], + ellipsis: true, + filters: true, + onFilter: true, + width:100, + valueType: 'select', + filterSearch: true + }, + { + title: '更新时间', + dataIndex: 'updateTime', + ellipsis:true, + width:182, + sorter: (a,b)=> { + return a.updateTime.localeCompare(b.updateTime) + }, + }, +]; + +export enum ClusterStatusEnum { + '异常', + '正常' +} + +export const DASHBOARD_SETTING_DRIVER_OPTION_LIST = [ + { + label:'influxdb', + value:'influxdb-v2' + } +] \ No newline at end of file diff --git a/frontend/packages/core/src/const/partitions/types.ts b/frontend/packages/core/src/const/partitions/types.ts new file mode 100644 index 00000000..dab84baa --- /dev/null +++ b/frontend/packages/core/src/const/partitions/types.ts @@ -0,0 +1,109 @@ + +import { EntityItem } from "@common/const/type"; + +export type PartitionConfigFieldType = { + name?: string; + id?: string; + description?: string; + prefix?:string + url?:string + managerAddress?:string + canDelete?:boolean +}; + +export type PartitionCertTableListItem = { + id:string; + name: string; + domains:string[]; + notAfter:string; + notBefore:string; + updater:EntityItem; + updateTime:string; +}; + +export type PartitionCertConfigFieldType = { + id?:string + key:string + pem:string +}; + +export type PartitionCertConfigProps = { + type:'add'|'edit' + entity?:PartitionCertConfigFieldType +} + +export type PartitionCertConfigHandle = { + save:()=>Promise +} + +export type PartitionClusterFieldType = { + name?: string; + id?: string; + description?: string; + address?:string; + protocol?:'http'|'https' +}; + +export type ClusterConfigProps = { + mode:'config' | 'retry' | 'result' | 'edit', + clusterId?:string + initFormValue?:{[k:string]:string|number} +} + +export type ClusterConfigHandle = { + save:()=>Promise + check:()=>Promise +} + +export type PartitionClusterTableListItem = { + id:string; + name: string; + status:0|1; + description:string; +}; + +export type PartitionClusterNodeTableListItem = { + id:string; + name: string; + managerAddress:string[]; + serviceAddress:string[]; + peerAddress:string; + status:0|1; +}; + +export type PartitionClusterNodeModalTableListItem = { + id: string, + name: string, + managerAddress: [], + serviceAddress: [], + peerAddress: string, + status: string +} + +export type NodeModalFieldType = { + address:string +} + + +export type NodeModalHandle = { + save:()=>Promise +} + +export type PartitionTableListItem = { + id:string; + name: string; + clusterNum:number; + updater:EntityItem; + updateTime:string; +}; + +export type SimplePartition = EntityItem & { clusters: (EntityItem & {description:string})[] } + +export type PartitionDashboardConfigFieldType = { + driver:string + config:{ + org:string + token:string + addr:string + } +} \ No newline at end of file diff --git a/frontend/packages/core/src/const/role/const.tsx b/frontend/packages/core/src/const/role/const.tsx new file mode 100644 index 00000000..18afbf5d --- /dev/null +++ b/frontend/packages/core/src/const/role/const.tsx @@ -0,0 +1,14 @@ + +export const ROLE_TABLE_COLUMNS = [ + { + title: '角色名称', + dataIndex: 'name', + copyable: true, + ellipsis:true, + fixed:'left', + sorter: (a,b)=> { + return a.name.localeCompare(b.name) + }, + } + +] \ No newline at end of file diff --git a/frontend/packages/core/src/const/role/type.ts b/frontend/packages/core/src/const/role/type.ts new file mode 100644 index 00000000..3fa66a30 --- /dev/null +++ b/frontend/packages/core/src/const/role/type.ts @@ -0,0 +1,14 @@ + +export type RoleTableListItem = { + id:string + name:string +} + +export type RoleModalContentProps = { + type:'add'|'edit' + entity?:RoleTableListItem +} + +export type RoleModalContentHandle = { + save:()=>Promise +} \ No newline at end of file diff --git a/frontend/packages/core/src/const/system-running/const.ts b/frontend/packages/core/src/const/system-running/const.ts new file mode 100644 index 00000000..151beda9 --- /dev/null +++ b/frontend/packages/core/src/const/system-running/const.ts @@ -0,0 +1,80 @@ +export const BASE_GROUP_ORDER = JSON.stringify({}) + +/** + * 应用关系图节点字体大小 + */ +export const RELATIVE_PICTURE_NODE_FONTSIZE = 14 + +export const SELF_SPACE_THEME = '#5B8FF933' +export const OUT_SPACE_THEME = '#F19E5733' +export const SELF_SPACE_CONTENT_COLOR = '#5B8FF9' +export const OUT_SPACE_CONTENT_COLOR = '#F19E57' +export const SELF_SPACE_CONTENT_EDGE_COLOR = '#5B8FF980' +export const OUT_SPACE_CONTENT_EDGE_COLOR = '#F19E5780' + +export const END_ARROW_STYLE = { + path: 'M 0,0 L 12,6 L 9,0 L 12,-6 Z', + fill: '#E2E2E2', + zIndex: 999 +} +export const EDGE_STYLE = { + stroke: '#E2E2E2', + endArrow: END_ARROW_STYLE, + lineWidth: 2, + cursor: 'pointer' +} + +export const SYSTEM_TUNNING_CONFIG = { + options: { + style: EDGE_STYLE + }, + + afterDraw: (cfg, group) => { + const lineDash = [4, 2, 1, 2] + if (!group) return + const shape = group.get('children')[0] + let index = 0 + // Define the animation + shape.animate( + () => { + index = index + 0.4 + if (index > 1000) { + index = 0 + } + const res = { + lineDash, + lineDashOffset: -index + } + return res + }, + { + repeat: true, + duration: 5000 + } + ) + }, + + setState: (name, value, item) => { + if (!item || !name) return + const shape = item.get('keyShape') + const itemStatus = item.getStates() + + if ( + !['edge-success', 'edge-error', 'edge-transparent'].includes(name) && + itemStatus.some((state) => ['edge-error', 'edge-success', 'edge-transparent'].includes(state)) + ) + return + const theme = item?._cfg?.model?.style?.stroke || SELF_SPACE_THEME + if (name === 'running') { + if (value) { + shape.attr({ + lineWidth: 4, + shadowColor: theme, + shadowBlur: 2 + }) + } else { + shape.attr(EDGE_STYLE) + } + } + } +} diff --git a/frontend/packages/core/src/const/system-running/type.ts b/frontend/packages/core/src/const/system-running/type.ts new file mode 100644 index 00000000..58673fb0 --- /dev/null +++ b/frontend/packages/core/src/const/system-running/type.ts @@ -0,0 +1,225 @@ +import { EdgeConfig, NodeConfig } from "@antv/g6" + +/** + * 获取项目组拓扑关联关系列表接口请求 + */ +export interface GetProjectGroupRelativeRequest { + spaceId: string + } + + /** + * 获取项目组关联关系请求体 + */ + export interface ProjectGroupRelativeRequest { + projectId: string + } + + /** + * 获取项目组关联关系返回体 + */ + export interface ProjectGroupRelativeResponse { + success: boolean + code: number + message: string + requestId: string + data: Data + } + + + export interface Node { + id: object + workSpaceId: object + projectId: string + workSpaceName: string + name: string + status: number + createTime: object + updateTime: object + // 可以配置任意 css + labelCfg: unknown + } + + export interface Edge { + id: object + startProjectId: string + endProjectId: string + status: number + createTime: object + updateTime: object + } + + /** + * 获取空间应用(项目组)关联关系请求体 + */ + export interface GetSpaceProjectGroupRelativeRequest { + spaceId: string + } + + /** + * 获取空间应用(项目组)关联关系返回体 + */ + export interface GetSpaceProjectGroupRelativeResponse { + success: boolean + code: number + message: string + requestId: string + data: Data + } + + export interface GraphData { + edges: EdgeConfig[] + nodes: NodeData[]|NodeConfig[] + } + + export interface Nodes { + id: object + workSpaceId: object + projectId: string + name: string + status: number + createTime: object + workSpaceName: string + updateTime: object + // 可以配置任意 css + labelCfg: unknown + } + + export interface NodeClickItem { + id: string + } + + export interface Edge { + id: object + startProjectId: string + endProjectId: string + status: number + createTime: object + updateTime: object + } + + /** + * 获取项目组拓扑关联关系列表接口返回 + */ + export interface GetProjectGroupRelativeResponse { + data: ProjectGroupRelativeItem[] + success: boolean + code: number + message: string + requestId: string + } + + /** + * 获取项目组拓扑关联关系列表数据 + */ + export interface ProjectGroupRelativeItem { + // 这个返回字段有误解,实际对应的内容为项目组,反馈过但 java 那边发脾气没改 + projectId: string + projectName: string + upstreamCount: number + workSpaceName: string + downstreamCount: number + id: number + } + + /** + * 侧边分组树节点的基本数据结构 + */ + export interface ProjectGroupTreeItem { + groupDepth: number + groupID: number | string + groupName: string + downstreamCount: number | string + parentGroupID: number + upstreamCount: number | string + groupOrder: string + groupPath: string + showSelectedTag: boolean + } + + /** + * 视图枚举 global:全局视图 + * part:局部视图(焦点视图) + */ + export enum PictureTypeEnum { + Global = 'global', + Part = 'part' + } + + export type RelativeItem = { + label: string + value: string + } + + export type RelativeInfo = { + name: string + id: string + } + + export interface RelativeListRequest { + sourceProjectId: string + targetProjectId: string + sourceApiNodeIds?: number[] + targetApiNodeIds?: number[] + } + + export interface ApiRelativeResponse { + success: boolean + code: number + message: string + requestId: string + data: Data + } + + export interface Data { + sourceApiList: SourceApiItem[] + targetApiList: TargetApiItem[] + } + + type TargetApiItem = SourceApiItem + + export interface SourceApiItem { + id: number + apiId: string + projectId: object + workspaceId: object + name: string + uri: string + apiProtocol: string + apiRequestType: string + apiType: string + previewPermission: number + apiRelationPermission: number + projectHashKey: string + groupID: string + spaceKey: string + } + + export interface ApiRelativeCache { + data: SourceApiItem[] + total: number + } + + +export type NodeData = { + id: string + selected?: boolean + label: string + name: string + // 此节点是否为自身空间的样式 + isSelfSpace: boolean + color?: string + state?: string + // 可以任意样式 + style?: unknown + // 可以任意样式 + stateStyles?: unknown + x?: number + y?: number + title?: string + preRect?: { + show: boolean + width: number + fill: string + radius: number + } + } \ No newline at end of file diff --git a/frontend/packages/core/src/const/system/const.tsx b/frontend/packages/core/src/const/system/const.tsx new file mode 100644 index 00000000..0054011e --- /dev/null +++ b/frontend/packages/core/src/const/system/const.tsx @@ -0,0 +1,950 @@ +import { ProColumns } from "@ant-design/pro-components"; +import { GlobalNodeItem, MyServiceTableListItem, NodeItem, ProxyHeaderItem, ServiceApiTableListItem, SimpleApiItem, SystemApiTableListItem, SystemAuthorityTableListItem, SystemMemberTableListItem, SystemSubServiceTableListItem, SystemSubscriberTableListItem, SystemTableListItem, SystemUpstreamTableListItem } from "./type"; +import { Input, InputNumber, MenuProps, Select, TabsProps, Tooltip } from "antd"; +import { ColumnsType } from "antd/es/table"; +import { getItem } from "@common/utils/navigation"; +import { MatchItem, MemberItem } from "@common/const/type"; +import { ConfigField } from "@common/components/aoplatform/EditableTableWithModal"; +import { frontendTimeSorter } from "@common/utils/dataTransfer"; +import moment from "moment"; +import { STATUS_COLOR } from "@common/const/const"; +import { LoadingOutlined } from "@ant-design/icons"; +import { SystemInsidePublishOnlineItems } from "../../pages/system/publish/SystemInsidePublishOnline"; +import { Link } from "react-router-dom"; + +export enum SubscribeEnum{ + '驳回' = 0, + '审核中' = 1, + '已订阅' = 2, + '取消订阅' = 3, + '取消申请' = 4 +} + + +export const SubscribeStatusColor= { + 2: 'text-[#138913]', // 使用 Tailwind 的 Arbitrary Properties + 1: 'text-[#03a9f4]', + 0: 'text-[#ff3b30]', + 3: 'text-[#ff3b30]', + 4: 'text-[#ff3b30]', + }; + +export enum SubscribeFromEnum { + '手动添加' = 0, + '订阅申请' = 1 +} + + +export enum MatchPositionEnum { + 'header' = 'HTTP 请求头', + 'query' = '请求参数', + 'cookie' = 'Cookie' +} + +export enum MatchTypeEnum{ + 'EQUAL' = '全等匹配', + 'PREFIX' = '前缀匹配', + 'SUFFIX' = '后缀匹配', + 'SUBSTR' = '子串匹配', + 'UNEQUAL' = '非等匹配', + 'NULL' = '空值匹配', + 'EXIST' = '存在匹配', + 'UNEXIST'='不存在匹配', + 'REGEXP'='区分大小写的正则匹配', + 'REGEXPG'='不区分大小写的正则匹配', + 'unknown'='任意匹配' +} + +export const HTTP_METHOD = ['GET','POST','PUT','DELETE','PATCH','HEAD'] + + +export const ALGORITHM_ITEM = [ + {label:'HS256',value:'HS256'}, + {label:'HS384',value:'HS384'}, + {label:'HS512',value:'HS512'}, + {label:'RS256',value:'RS256'}, + {label:'RS384',value:'RS384'}, + {label:'RS512',value:'RS512'}, + {label:'ES256',value:'ES256'}, + {label:'ES384',value:'ES384'}, + {label:'ES512',value:'ES512'}, +] + +export const SYSTEM_TABLE_COLUMNS: ProColumns[] = [ + { + title: '服务名称', + dataIndex: 'name', + copyable: true, + ellipsis:true, + width:160, + fixed:'left', + sorter: (a,b)=> { + return a.name.localeCompare(b.name) + }, + }, + { + title: '服务 ID', + dataIndex: 'id', + width: 140, + copyable: true, + ellipsis:true, + }, + { + title: '所属团队', + dataIndex: ['team','name'], + copyable: true, + ellipsis:true, + // filters: true, + // onFilter: true, + // filterSearch: true, + }, + { + title: 'API 数量', + dataIndex: 'apiNum', + ellipsis:true, + sorter: (a,b)=> { + return a.apiNum - b.apiNum + }, + }, + { + title: '描述', + dataIndex: 'description', + ellipsis:true, + }, + { + title: '负责人', + dataIndex: ['master','name'], + ellipsis: true, + width:108, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true, + }, + { + title: '创建时间', + dataIndex: 'createTime', + width:182, + ellipsis:true, + sorter: (a,b)=>frontendTimeSorter(a,b,'createTime') + } +]; + +export const SYSTEM_SUBSERVICE_TABLE_COLUMNS: ProColumns[] = [ + { + title: '服务名称', + dataIndex: ['service','name'], + copyable: true, + ellipsis:true, + width:160, + fixed:'left', + sorter: (a,b)=> { + return a.service.name.localeCompare(b.service.name) + }, + }, + { + title: '服务 ID', + dataIndex: ['service','name'], + width: 140, + copyable: true, + ellipsis:true + }, + { + title: '申请状态', + dataIndex: 'applyStatus', + ellipsis:{ + showTitle:true + }, + width:80, + filters: true, + onFilter: true, + valueType: 'select', + valueEnum:new Map([ + [0,驳回], + [1,审核中], + [2,已订阅], + [3,取消订阅], + [4,取消申请], + ]) + }, + { + title: '所属服务', + dataIndex: ['project','name'], + copyable: true, + ellipsis:true + }, + { + title: '所属团队', + dataIndex: ['team','name'], + copyable: true, + ellipsis:true + }, + { + title: '申请人', + dataIndex: ['applier','name'], + ellipsis: true, + width:88, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true, + }, + { + title: '来源', + dataIndex: 'from', + ellipsis: true, + filters: true, + onFilter: true, + valueType: 'select', + valueEnum:new Map([ + [0,手动添加], + [1,订阅申请], + ]) + }, + { + title: '添加时间', + dataIndex: 'createTime', + ellipsis:true, + width:182, + sorter: (a,b)=> { + return a.createTime.localeCompare(b.createTime) + }, + }, +]; + + +export const SYSTEM_SUBSCRIBER_TABLE_COLUMNS: ProColumns[] = [ + { + title: '服务名称', + dataIndex: ['service','name'], + copyable: true, + ellipsis:true, + width:160, + fixed:'left', + sorter: (a,b)=> { + return a.service.name.localeCompare(b.service.name) + }, + }, + { + title: '服务 ID', + dataIndex: 'id', + width: 140, + copyable: true, + ellipsis:true + }, + { + title: '订阅方', + dataIndex: ['subscriber','name'], + copyable: true, + ellipsis:true + }, + { + title: '所属团队', + dataIndex: ['team','name'], + copyable: true, + ellipsis:true + }, + // { + // title: '申请人', + // dataIndex: ['applier','name'], + // ellipsis:true, + // width:88, + // filters: true, + // onFilter: true, + // valueType: 'select', + // filterSearch: true, + // }, + // { + // title: '审批人', + // dataIndex: ['approver','name'], + // ellipsis:true, + // width:88, + // filters: true, + // onFilter: true, + // valueType: 'select', + // filterSearch: true, + // }, + { + title: '来源', + dataIndex: 'from', + ellipsis:true, + filters: true, + onFilter: true, + valueType: 'select', + valueEnum:new Map([ + [0,手动添加], + [1,订阅申请], + ]) + }, + { + title: '订阅时间', + dataIndex: 'applyTime', + ellipsis:true, + width:182, + sorter: (a,b)=> { + return a.applyTime.localeCompare(b.applyTime) + }, + }, +]; + + +export const memberModalColumn:ColumnsType = [ + {title:'成员', + render:(_,entity)=>{ + return <> +
+

+ {entity.name} + {entity.email !== undefined && {entity.email}} +

+

{entity.department}

+
+ + }} +] + +export const SYSTEM_MEMBER_TABLE_COLUMN: ProColumns[] = [ + { + title: '用户名', + dataIndex: ['user','name'], + copyable: true, + ellipsis:true, + width:160, + fixed:'left', + sorter: (a,b)=> { + return a.user.name.localeCompare(b.user.name) + }, + }, + { + title: '邮箱', + dataIndex: 'email', + copyable: true, + ellipsis:true + }, + { + title: '角色', + dataIndex: ['roles','name'], + copyable: true, + ellipsis:true + + } +]; + +export const MATCH_CONFIG:ConfigField[] = [ + { + title: '参数位置', + key: 'position', + component: , + renderText: (value: unknown) => <>{value}, + required: true + }, { + title: '匹配类型', + key: 'matchType', + component: , + renderText: (value: string) => { + return (<>{value}) + }, + required: true + } +] + + +export const SYSTEM_API_TABLE_COLUMNS: ProColumns[] = [ + { + title: '名称', + dataIndex: 'name', + copyable: true, + ellipsis:true, + width:160, + fixed:'left', + valueType: 'text', + sorter: (a,b)=> { + return a.name.localeCompare(b.name) + }, + }, + { + title: '协议/方法', + dataIndex: 'method', + ellipsis:true, + filters: true, + onFilter: true, + valueType: 'select', + valueEnum: { + POST: { text: 'POST' }, + PUT: { text: 'PUT' }, + GET: { text: 'GET' }, + DELETE: { text: 'DELETE' }, + PATCH: { text: 'PATCH' }, + }, + }, + { + title: 'URL', + dataIndex: 'requestPath', + copyable: true, + ellipsis:true + }, + { + title: '创建者', + dataIndex: ['creator','name'], + ellipsis: true, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true, + }, + { + title: '更新时间', + dataIndex: 'updateTime', + ellipsis:true, + hideInSearch: true, + width:182, + sorter: (a,b)=>frontendTimeSorter(a,b,'updateTime') + }, +]; + + + +export const SYSTEM_UPSTREAM_TABLE_COLUMNS: ProColumns[] = [ + { + title: '名称', + dataIndex: 'name', + copyable: true, + ellipsis:true, + width:160, + fixed:'left', + sorter: (a,b)=> { + return a.name.localeCompare(b.name) + }, + }, + { + title: '上游 ID', + dataIndex: 'id', + width: 140, + copyable: true, + ellipsis:true + }, + { + title: '创建人', + dataIndex: ['creator','name'], + ellipsis: true, + width:88, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true, + }, + { + title: '更新人', + dataIndex: ['updater','name'], + ellipsis: true, + width:88, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true, + }, + { + title: '创建时间', + dataIndex: 'createTime', + width:182, + ellipsis:true, + sorter: (a,b)=> { + return a.createTime.localeCompare(b.createTime) + } + }, + { + title: '更新时间', + dataIndex: 'updateTime', + width:182, + ellipsis:true, + sorter: (a,b)=> { + return a.updateTime.localeCompare(b.updateTime) + }, + }, +]; + +export enum UpstreamDriverEnum{ + 'static'='静态上游', + 'discoveries'='动态服务发现', +} + +export const typeOptions = [ + { label: '静态上游', value: 'static' }, + // { label: '动态服务发现', value: 'discoveries' }, +]; + +export const schemeOptions = [ + { label:'HTTPS', value:'HTTPS'}, + { label:'HTTP', value:'HTTP'}, +] +export const balanceOptions = [ + { label: '带权轮询', value: 'round-robin' }, + { label: 'IP Hash', value: 'ip-hash' }, +]; + +export const passHostOptions = [ + { label:'透传客户端请求 Host', value:'pass'}, + { label:'使用上游服务 Host', value:'node'}, + { label:'重写 Host', value:'rewrite'}, +] + +export const proxyHeaderTypeOptions =[ + {label:'新增或修改', value: 'ADD' }, + { label: '删除', value: 'DELETE' } +] + +export const PROXY_HEADER_CONFIG:ConfigField[] = [ + { + title: '操作类型', + key: 'optType', + component: , + renderText: (value: string) => { + return (<>{value}) + }, + required: true + }, { + title: '参数值', + key: 'value', + component: , + renderText: (value: string) => { + return (<>{value}) + }, + required: true + } +] + +export const NODE_CONFIG:ConfigField[] = [ + { + title: '集群', + key: 'cluster', + component: , + renderText: (value: string) => { + return (<>{value}) + }, + required: true + }, { + title: '权重', + key: 'weight', + component: , + renderText: (value: string) => { + return (<>{value}) + }, + required: true + } +] + +export const visualizations = [ + {label:'内部服务:可通过网关访问,但不展示在服务广场',value:'inner'}, + {label:'公开服务:可通过网关访问,展示在服务广场,可被其他应用订阅',value:'public'}]; + + + +export const SYSTEM_MYSERVICE_API_TABLE_COLUMNS: ProColumns[] = [ + { + title: ' ', + dataIndex: 'id', + width:'40px', + fixed:'left' + }, + { + title: '名称', + dataIndex: 'name', + copyable: true, + width:160, + fixed:'left', + ellipsis:true + }, + { + title: '请求方式', + dataIndex: 'method', + ellipsis:true + }, + { + title: '请求路径', + dataIndex: 'path', + copyable: true, + ellipsis:true + }, + { + title: '描述', + dataIndex: 'description', + copyable: true, + ellipsis:true + } +]; + + +export const apiModalColumn:ColumnsType = [ + { + title:'所有 API', + dataIndex:'method', + }, + { + title:'', + dataIndex:'name', + ellipsis:true + } +] + + +export const SYSTEM_AUTHORITY_TABLE_COLUMNS: ProColumns[] = [ + { + title: '名称', + dataIndex: 'name', + copyable: true, + ellipsis:true, + width:160, + fixed:'left', + sorter: (a,b)=> { + return a.name.localeCompare(b.name) + }, + }, + { + title: '类型', + dataIndex: 'driver', + ellipsis:true, + filters: true, + onFilter: true, + valueType: 'select', + valueEnum:{ + basic:{ + text:'Basic' + }, + apikey:{ + text:'Apikey' + } + } + }, + { + title: '隐藏鉴权信息', + dataIndex: 'hideCredential', + ellipsis:{ + showTitle:true + }, + filters: true, + onFilter: true, + valueType: 'select', + valueEnum:new Map([ + [true,], + [false,], + ]) + }, + { + title: '过期时间', + dataIndex: 'expireTime', + ellipsis:true, + width:182, + render:(_: React.ReactNode, entity: SystemAuthorityTableListItem) => ( + 0 ? 'text-status_fail' : ''}>{entity.expireTime === 0 ? '永不过期' :moment(entity.expireTime * 1000).format('YYYY-MM-DD hh:mm:ss')} + ), + sorter: (a,b)=> { + return a.expireTime - b.expireTime + }, + }, + { + title: '更新者', + dataIndex: ['updater','name'], + ellipsis: true, + width:88, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true, + }, + { + title: '创建时间', + key: 'createTime', + dataIndex: 'createTime', + width:182, + ellipsis:true, + sorter: (a,b)=> { + return a.createTime.localeCompare(b.createTime) + }, + }, +]; + + +export const SYSTEM_MYSERVICE_TABLE_COLUMNS: ProColumns[] = [ + { + title: '服务名称', + dataIndex: 'name', + copyable: true, + ellipsis:true, + width:160, + fixed:'left', + sorter: (a,b)=> { + return a.name.localeCompare(b.name) + }, + }, + { + title: '服务ID', + dataIndex: 'id', + width: 140, + copyable: true, + ellipsis:true + }, + { + title: '服务类型', + dataIndex: 'serviceType', + ellipsis:{ + showTitle:true + }, + filters: true, + onFilter: true, + valueType: 'select', + valueEnum:{ + 'public':{ + text:'公开服务' + }, + 'inner':{ + text:'内部服务' + } + } + }, + { + title: 'API 数量', + dataIndex: 'apiNum', + sorter: (a,b)=> { + return a.apiNum - b.apiNum + }, + }, + { + title: '状态', + dataIndex: 'status', + ellipsis:{ + showTitle:true + }, + filters: true, + onFilter: true, + valueType: 'select', + valueEnum:{ + 'on':启用 , + 'off':停用 + } + }, + { + title: '更新时间', + key: 'updateTime', + dataIndex: 'updateTime', + ellipsis:true, + width:182, + sorter: (a,b)=> { + return a.updateTime.localeCompare(b.updateTime) + }, + }, + { + title: '创建时间', + key: 'createTime', + dataIndex: 'createTime', + width:182, + ellipsis:true, + sorter: (a,b)=> { + return a.createTime.localeCompare(b.createTime) + }, + }, +]; + + + +export const SYSTEM_UPSTREAM_GLOBAL_CONFIG_TABLE_COLUMNS: ProColumns[] = [ + { + title: '地址(IP 端口或域名)', + dataIndex: 'address', + width: '50%', + formItemProps: { + className:'p-0 bg-transparent border-none', + rootClassName:'test', + rules: [ + { + required: true, + whitespace: true, + message: '此项是必填项', + }, + ], + }, + copyable: true, + ellipsis:true + }, + { + title: '权重(0-999)', + dataIndex: 'weight', + valueType:'digit', + formItemProps: { + className:'p-0 bg-transparent border-none'} + }, + { + title: '操作', + valueType: 'option', + width: 90, + render: ()=>null + }, + ]; + + +export const SYSTEM_INSIDE_APPROVAL_TAB_ITEMS: TabsProps['items'] = [ + { + key: '0', + label: '待审批', + }, + { + key: '1', + label: '已审批', + } +]; + + + +export const SYSTEM_PUBLISH_TAB_ITEMS: TabsProps['items'] = [ + { + key: '0', + label: '发布版本', + }, + { + key: '1', + label: '发布申请记录', + } +]; + + +export const SYSTEM_SUBSCRIBE_APPROVAL_DETAIL_LIST = [ + { + title:'服务名称',key:'service',nested:'name' + }, + { + title:'服务 ID',key:'applyTeam',nested:'id' + }, + { + title:'所属团队',key:'team',nested:'name' + }, + { + title:'所属系统',key:'project',nested:'name' + }, + { + title:'申请状态',key:'status',renderText:()=>{} + }, + { + title:'申请人',key:'applier',nested:'name' + }, + // { + // title:'审批人',key:'team',nested:'name' + // }, + { + title:'申请时间',key:'applyTime' + }, + // { + // title:'审批时间',key:'team' + // } +] + +export const SYSTEM_TOPOLOGY_NODE_TYPE_COLOR_MAP = { + subscriberProject:{ + stroke:'#3291F8FF', + fill: '#3291F8FF', + name:'调用系统名称' + }, + subscriberService:{ + stroke:'#3D46F2', + fill: '#7371FC33', + name:'调用服务名称' + }, + curProject:{ + stroke:'#7371FCFF', + fill: '#7371FCFF', + name:'当前系统名称' + }, + invokeService:{ + stroke:'#3D46F2', + fill: '#7371FC33', + name:'被调用服务名称' + }, + invokeProject:{ + stroke:'#19C56BFF', + fill: '#19C56BFF', + name:'被调用系统名称' + }, + application:{ + stroke:'#ffa940', + fill: '#ffa94033', + } + }; + + + + export const SYSTEM_PUBLISH_ONLINE_COLUMNS = [ + { + title: '上线结果', + dataIndex: 'status', + ellipsis:{ + showTitle:true + }, + render:(_:unknown,entity:SystemInsidePublishOnlineItems)=>{ + switch(entity.status){ + case 'done': + return 成功 + case 'error': + return 失败 {entity.error} + default: + return + } + } + }, + ] + +const APP_MODE = import.meta.env.VITE_APP_MODE; + + + export const SYSTEM_PAGE_MENU_ITEMS: MenuProps['items'] = [ + getItem('内部数据服务', 'assets', null, + [ + getItem(API, 'api',undefined,undefined,undefined,'team.service.api.view'), + getItem(上游, 'upstream',undefined,undefined,undefined,'team.service.upstream.view'), + getItem(使用说明, 'document',undefined,undefined,undefined,''), + getItem(发布, 'publish',undefined,undefined,undefined,'team.service.release.view'), + ], + 'group'), + getItem('提供服务', 'provideSer', null, + [ + getItem(订阅审批, 'approval',undefined,undefined,undefined,'team.service.subscription.view'), + getItem(订阅方管理, 'subscriber',undefined,undefined,undefined,'team.service.subscription.view'), + ], + 'group'), + getItem('管理', 'mng', null, + [ + APP_MODE === 'pro' ? getItem(调用拓扑图, 'topology',undefined,undefined,undefined,'project.mySystem.topology.view'):null, + getItem(设置, 'setting',undefined,undefined,undefined,'')], + 'group'), +]; diff --git a/frontend/packages/core/src/const/system/type.ts b/frontend/packages/core/src/const/system/type.ts new file mode 100644 index 00000000..2a15aa74 --- /dev/null +++ b/frontend/packages/core/src/const/system/type.ts @@ -0,0 +1,441 @@ + +import { FormInstance, UploadFile } from "antd"; +import { HeaderParamsType, BodyParamsType, QueryParamsType, RestParamsType, ResultListType, ApiBodyType } from "@common/const/api-detail"; +import { EntityItem, MatchItem } from "@common/const/type"; +import { SubscribeEnum, SubscribeFromEnum } from "./const"; +import { HTTPMethod, Protocol } from "@common/components/postcat/api/RequestMethod"; + +export type SystemTableListItem = { + id:string; + name: string; + team: EntityItem; + apiNum: number; + serviceNum: number, + description:string; + master:EntityItem; + createTime:string; +}; + +export type SystemConfigFieldType = { + name?: string; + id?: string; + prefix?:string; + logo?:string; + logoFile?:UploadFile; + tags?:Array; + description?: string; + team?:string; + master?:string; + serviceType?:'public'|'inner'; + catalogue?:string | string[]; +}; + +export type SystemSubServiceTableListItem = { + id:string; + applyStatus:SubscribeEnum; + project:EntityItem; + team:EntityItem + service:EntityItem + applier:EntityItem + from:SubscribeFromEnum + createTime:string +}; + + + +export type SystemSubscriberTableListItem = { + id:string + service:EntityItem + applyStatus:SubscribeEnum + project:EntityItem + team:EntityItem; + applier:EntityItem + approver:EntityItem; + from:SubscribeFromEnum + applyTime:string +}; + +export type SystemSubscriberConfigFieldType = { + service:string + subscriber:string + applier:string +}; + +export type SystemSubscriberConfigProps = { + serviceId:string + teamId:string +} + +export type SystemSubscriberConfigHandle = { + save:()=>Promise +} + +export type SystemMemberTableListItem = { + user: EntityItem; + email:string; + roles:Array; + canDelete:boolean +}; + +export type SystemApiDetail = { + id:string + name:string + description:string + protocol:Protocol + method:HTTPMethod + path:string + creator:EntityItem + createTime:string + updater:EntityItem + updateTime:string + match?:MatchItem[] + proxy?:SystemApiProxyType + doc?:{ + encoding: string, + tag: string, + requestParams: { + headerParams: HeaderParamsType[], + bodyParams: BodyParamsType[], + queryParams: QueryParamsType[], + restParams: RestParamsType[] + }, + resultList: ResultListType[], + responseList: [{ + id: number, + responseUuid: string, + apiUuid: string, + oldId: number, + name: string, + httpCode: string, + contentType: ApiBodyType, + isDefault: number, + updateUserId: number, + createUserId: number, + createTime: number, + updateTime: number, + responseParams: { + headerParams: HeaderParamsType[], + bodyParams: BodyParamsType[] + queryParams: QueryParamsType[], + restParams: RestParamsType[] + } + }] + } +} + + +export type SystemApiProxyType = { + path:string + timeout:number + retry:number + headers:Array + +} +export type SystemApiProxyFieldType = { + name: string; + id:string; + description?:string; + path:string; + method:string; + match:MatchItem[] + isDisable?: boolean; + service?:string; + proxy:SystemApiProxyType +}; + +export type SystemApiSimpleFieldType = { + id: string + name: string + description: string + method: string + path: string + match: MatchItem[] + creator: string + updater: string + create_time: string + update_time: string +} + +export type SystemInsideApiCreateProps = { + type?:'copy' + entity?:SystemApiProxyFieldType &{systemId:string} + modalApiPrefix?:string + modalPrefixForce?:boolean + serviceId:string + teamId:string +} + +export type SystemInsideApiCreateHandle = { + copy:()=>Promise; + save:()=>Promise; +} + + +export type SystemApiTableListItem = { + id:string; + name: string; + method:string; + requestPath:string; + creator:EntityItem; + createTime:string; + updater:EntityItem + updateTime:string + canDelete:boolean +}; + + +export type EditAuthFieldType = { + id?:string + name: string + driver: string + hideCredential: boolean + expireTime: number + position: string + tokenName: string + config: { + userName?: string + password?: string + apikey?: string + ak?: string + sk?: string + iss?: string + algorithm?: string + secret?: string + publicKey?: string + user?: string + userPath?: string + claimsToVerify?: string[] + signatureIsBase64?: boolean + } +} + + +export type SystemUpstreamTableListItem = { + name: string; + id:string; + driver:string; + creator:EntityItem; + updater:EntityItem; + createTime:string; + updateTime:string; + canDelete:boolean +}; + +export type ProxyHeaderItem = { + key:string + value:string + optType:string + id?:string +} + +export type GlobalNodeItem = { + address:string + weight:number +} + +export type NodeItem = Partial & { + cluster:string + clusterName?:string + _id?:string } + +export type DiscoverItem = { + cluster:string + service:string + discover:string +} + +export type ServiceUpstreamFieldType = { + driver:string + nodes:GlobalNodeItem[], + discover?:DiscoverItem + timeout:number; + retry?:number; + limitPeerSecond?:number; + scheme:string, + passHost:string, + upstreamHost:string, + balance:string; + proxyHeaders:ProxyHeaderItem[] +}; + + +export type MyServiceFieldType = { + name?: string; + id?: string; + description?: string; + team?:string; + project?:string; + status?:'off'|'on' +}; + +export type SimpleSystemItem = { + id:string + name:string + team:EntityItem +} + +export type ServiceApiTableListItem = { + id:string; + name: string; + method:string; + path:string; + description:string; +}; + +export type SimpleApiItem = { + id:string + name:string + method:string + requestPath:string +} + +export type SystemAuthorityTableListItem = { + id:string + name: string; + driver:string; + hideCredential:boolean; + expireTime:number; + creator:EntityItem; + updater:EntityItem; + createTime:string; + updateTime:string +}; + +export type MyServiceTableListItem = { + id:string; + name: string; + serviceType:'public'|'inner'; + apiNum:number; + status:string; + createTime:string; + updateTime:string; +}; + + +export type SystemInsideApiDetailProps = { + serviceId:string; + teamId:string; + apiId:string; +} + + +export type SystemInsideApiDocumentHandle = { + save:()=>Promise|undefined +} + +export type SystemInsideApiDocumentProps = { + serviceId:string + teamId:string + apiId:string +} + + +export type SystemInsideApiProxyProps = { + className?:string + service:string + teamId:string + initProxyValue?:SystemApiProxyType + value?:SystemApiProxyType + onChange?: (newConfigItems: SystemApiProxyType) => void; // 当配置项变化时,外部传入的回调函数 +} + +export type SystemInsideApiProxyHandle = { + validate:()=>Promise +} + + +export interface MyServiceInsideConfigHandle { + save:()=>Promise +} + +export interface MyServiceInsideConfigProps { + + teamId:string + serviceId?:string + closeDrawer?:() => void +} + + +export type SubSubscribeApprovalModalProps = { + type:'reApply'|'view' + data?:SystemSubServiceTableListItem + teamId:string + serviceId?:string +} + +export type SubSubscribeApprovalModalHandle = { + reApply:() =>Promise +} + +export type SubSubscribeApprovalModalFieldType = { + reason?:string; + opinion?:string; +}; + +export type SystemInsideUpstreamConfigProps = { + upstreamNameForm:FormInstance + setLoading:(loading:boolean) => void +} + +export type SystemInsideUpstreamConfigHandle = { + save:()=>Promise|undefined +} + +export type SystemInsideUpstreamContentHandle = { + save:()=>Promise|undefined +} + + +export type SystemConfigHandle = { + save:()=>Promise|undefined +} + + +export type SystemTopologyServiceItem = EntityItem & { + project:string +} + +export interface SystemTopologySubscriber { + project: EntityItem; + services: EntityItem[]; + } + + export interface SystemTopologyInvoke { + project: EntityItem; + services: EntityItem[]; + } + + + // 接口返回的数据格式 + export interface SystemTopologyResponse { + services: SystemTopologyServiceItem[]; + subscribers: SystemTopologySubscriber[]; + invoke: SystemTopologyInvoke[]; + } + +export enum SystemReleaseStatus { + '正常' = 0, + '未设置' = 1, + '缺失' = 2 +} + + export type SystemPublishReleaseItem = { + api: Array<{ + name: string, + method: string, + path: string, + upstream: string, + change: string, + status: { + upstreamStatus: SystemReleaseStatus, + docStatus: SystemReleaseStatus, + proxyStatus: SystemReleaseStatus + } + }> + upstream: Array<{ + name: "", + type: "", + addr: [], + status: "" + }> + } \ No newline at end of file diff --git a/frontend/packages/core/src/const/team/const.tsx b/frontend/packages/core/src/const/team/const.tsx new file mode 100644 index 00000000..294ed9e1 --- /dev/null +++ b/frontend/packages/core/src/const/team/const.tsx @@ -0,0 +1,178 @@ + +import { ProColumns } from "@ant-design/pro-components"; +import { TeamMemberTableListItem, TeamTableListItem } from "./type"; +import { ColumnsType } from "antd/es/table"; +import { MemberItem } from "@common/const/type"; +import { getItem, getTabItem } from "@common/utils/navigation"; +import { SystemTableListItem } from "../system/type"; +import { MenuProps, TabsProps } from "antd/lib"; +import { Link } from "react-router-dom"; + +export const TEAM_TABLE_COLUMNS: ProColumns[] = [ + { + title: '名称', + dataIndex: 'name', + copyable: true, + ellipsis:true, + width:160, + fixed:'left', + sorter: (a,b)=> { + return a.name.localeCompare(b.name) + }, + }, + { + title: 'ID', + dataIndex: 'id', + width: 140, + copyable: true, + ellipsis:true + }, + { + title: '描述', + dataIndex: 'description', + copyable: true, + ellipsis:true + }, + { + title: '服务数量', + dataIndex: 'serviceNum', + ellipsis:true, + sorter: (a,b)=> { + return a.serviceNum - b.serviceNum + }, + }, + { + title: '负责人', + dataIndex: ['master','name'], + ellipsis: true, + width:108, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true, + }, + { + title: '创建时间', + dataIndex: 'createTime', + ellipsis:true, + width:182, + sorter: (a,b)=> { + return a.createTime.localeCompare(b.createTime) + }, + }, +]; + + +export const TEAM_SYSTEM_TABLE_COLUMNS: ProColumns[] = [ + { + title: '服务名称', + dataIndex: 'name', + copyable: true, + ellipsis:true, + width:160, + fixed:'left', + sorter: (a,b)=> { + return a.name.localeCompare(b.name) + }, + }, + { + title: '服务 ID', + dataIndex: 'id', + width: 140, + copyable: true, + ellipsis:true + }, + { + title: '所属团队', + dataIndex: ['team','name'], + copyable: true, + ellipsis:true + }, + { + title: 'API数量', + dataIndex: 'apiNum', + ellipsis:true, + sorter: (a,b)=> { + return a.apiNum - b.apiNum + }, + }, + { + title: '服务数量', + dataIndex: 'serviceNum', + ellipsis:true, + sorter: (a,b)=> { + return a.serviceNum - b.serviceNum + }, + }, + { + title: '负责人', + dataIndex: ['master','name'], + ellipsis: true, + width:108, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true + }, + { + title: '添加日期', + dataIndex: 'createTime', + ellipsis: true, + sorter: (a,b)=> { + return a.createTime.localeCompare(b.createTime) + }, + }, +]; + +export const TEAM_MEMBER_TABLE_COLUMNS: ProColumns[] = [ + { + title: '姓名', + dataIndex: ['user','name'], + copyable: true, + ellipsis:true, + width:160, + fixed:'left', + sorter: (a,b)=> { + return a.user.name.localeCompare(b.user.name) + }, + }, + { + title: '团队角色', + dataIndex: 'roles', + copyable: true, + ellipsis:true, + }, + { + title: '添加日期', + dataIndex: 'attachTime', + ellipsis:true, + sorter: (a,b)=> { + return a.attachTime.localeCompare(b.attachTime) + }, + }, +]; + + +export const TEAM_MEMBER_MODAL_TABLE_COLUMNS:ColumnsType = [ + {title:'成员', + render:(_,entity)=>{ + return <> +
+

+ {entity.name} + {entity.email !== undefined && {entity.email}} +

+

{entity.department || '-'}

+
+ + }} +] + + export const TEAM_INSIDE_MENU_ITEMS: MenuProps['items'] = [ + getItem('管理', 'grp', null, + [ + getItem(成员, 'member',undefined, undefined, undefined,'team.team.member.view'), + getItem(设置, 'setting',undefined,undefined,undefined,'team.team.team.edit')], + 'group'), + ]; + \ No newline at end of file diff --git a/frontend/packages/core/src/const/team/type.ts b/frontend/packages/core/src/const/team/type.ts new file mode 100644 index 00000000..e5dd334b --- /dev/null +++ b/frontend/packages/core/src/const/team/type.ts @@ -0,0 +1,42 @@ + +import { EntityItem } from "@common/const/type"; + +export type TeamTableListItem = { + id:string; + name: string; + description:string; + serviceNum:number; + creator:EntityItem; + createTime:string; + canDelete:boolean +}; + +export type TeamConfigProps = { + entity?:TeamConfigFieldType +} +export type TeamConfigHandle = { + save:()=>Promise +} + +export type TeamConfigType = { + name: string; + id?: string; + description: string; + master:EntityItem; + canDelete:boolean +}; + +export type TeamConfigFieldType = { + name: string; + id?: string; + description: string; + master:string; +}; + +export type TeamMemberTableListItem = { + user:EntityItem; + roles:EntityItem[]; + userGroup:EntityItem; + attachTime:string; + isDelete:boolean +}; \ No newline at end of file diff --git a/frontend/packages/core/src/const/user/const.tsx b/frontend/packages/core/src/const/user/const.tsx new file mode 100644 index 00000000..7b06d0d4 --- /dev/null +++ b/frontend/packages/core/src/const/user/const.tsx @@ -0,0 +1,55 @@ + +import { ProColumns } from "@ant-design/pro-components"; +import { ColumnsType } from "antd/es/table"; +import { MemberItem } from "@common/const/type"; +import { Tooltip } from "antd"; + +export const USER_LIST_COLUMNS: ProColumns[]= [ + { + title: '用户名', + dataIndex: 'name', + copyable: true, + ellipsis:true, + width:160, + fixed:'left', + sorter: (a,b)=> { + return a.name.localeCompare(b.name) + }, + }, + { + title: '邮箱', + dataIndex: 'email', + copyable: true, + ellipsis:true, + }, + { + title: '部门', + dataIndex: 'department', + ellipsis:{ + showTitle:true + }, + filterMode:'tree', + renderText:(_,entity)=>(entity.department?.map((x)=>x.name).join(',')||'-'), + filters: [], + onFilter: true, + valueType: 'select', + filterSearch: true, + } +]; + +export const MEMBER_MODAL_COLUMNS:ColumnsType = [ + {title:'所有成员', + width:215, + + render:(_,entity)=>{ + return <> + x.name).join(',') || '-'})`}> +
+ {entity.name} + {entity.email && ({entity.email})} + ({entity.department?.map(x=>x.name).join(',') || '-'}) +
+
+ + }} +] \ No newline at end of file diff --git a/frontend/packages/core/src/const/user/types.ts b/frontend/packages/core/src/const/user/types.ts new file mode 100644 index 00000000..a36e9b29 --- /dev/null +++ b/frontend/packages/core/src/const/user/types.ts @@ -0,0 +1,23 @@ +import { DepartmentListItem } from "../member/type"; + + +export type UserGroupItem = { + id:string + name:string + usage:number +} + +export type FieldType = { + id?:string + name:string +}; + +export type UserGroupModalProps = { + type:'add'|'edit' + entity?:FieldType + departmentList?:DepartmentListItem[] +} + +export type UserGroupModalHandle = { + save:()=>Promise +} diff --git a/frontend/packages/core/src/contexts/DashboardContext.tsx b/frontend/packages/core/src/contexts/DashboardContext.tsx new file mode 100644 index 00000000..96cd8a4f --- /dev/null +++ b/frontend/packages/core/src/contexts/DashboardContext.tsx @@ -0,0 +1,23 @@ + +import { createContext, useContext, useState, ReactNode, FC } from 'react'; +import {EntityItem} from "@common/const/type.ts"; + +interface DashboardContextProps { + currentClusterList:EntityItem[]; + setCurrentClusterList: React.Dispatch>; +} + +const DashboardContext = createContext(undefined); + +export const useDashboardContext = () => { + const context = useContext(DashboardContext); + if (!context) { + throw new Error('useArray must be used within a ArrayProvider'); + } + return context; +}; + +export const DashboardProvider: FC<{ children: ReactNode }> = ({ children }) => { + const [currentClusterList, setCurrentClusterList] = useState([]) + return {children}; +}; \ No newline at end of file diff --git a/frontend/packages/core/src/contexts/PartitionContext.tsx b/frontend/packages/core/src/contexts/PartitionContext.tsx new file mode 100644 index 00000000..2782ae2b --- /dev/null +++ b/frontend/packages/core/src/contexts/PartitionContext.tsx @@ -0,0 +1,23 @@ + +import {FC, createContext, useContext, useState, ReactNode } from 'react'; +import { PartitionConfigFieldType } from '../const/partitions/types.ts'; + +interface PartitionContextProps { + partitionInfo:PartitionConfigFieldType|undefined + setPartitionInfo:React.Dispatch>; +} + +const PartitionContext = createContext(undefined); + +export const usePartitionContext = () => { + const context = useContext(PartitionContext); + if (!context) { + throw new Error('useArray must be used within a ArrayProvider'); + } + return context; +}; + +export const PartitionProvider: FC<{ children: ReactNode }> = ({ children }) => { + const [partitionInfo, setPartitionInfo] = useState() + return {children}; +}; \ No newline at end of file diff --git a/frontend/packages/core/src/contexts/SystemContext.tsx b/frontend/packages/core/src/contexts/SystemContext.tsx new file mode 100644 index 00000000..f62125f8 --- /dev/null +++ b/frontend/packages/core/src/contexts/SystemContext.tsx @@ -0,0 +1,30 @@ + +import {FC, createContext, useContext, useState, ReactNode } from 'react'; +import { SystemConfigFieldType } from '../const/system/type.ts'; + +interface SystemContextProps { + apiPrefix:string; + setApiPrefix:React.Dispatch>; + prefixForce:boolean; + setPrefixForce:React.Dispatch>; + systemInfo:SystemConfigFieldType|undefined + setSystemInfo:React.Dispatch>; +} + +const SystemContext = createContext(undefined); + +export const useSystemContext = () => { + const context = useContext(SystemContext); + if (!context) { + throw new Error('useArray must be used within a ArrayProvider'); + } + return context; +}; + +export const SystemProvider: FC<{ children: ReactNode }> = ({ children }) => { + const [apiPrefix, setApiPrefix] = useState(''); + const [prefixForce, setPrefixForce] = useState(false); + const [systemInfo, setSystemInfo] = useState() + + return {children}; +}; \ No newline at end of file diff --git a/frontend/packages/core/src/contexts/SystemMyServiceContext.tsx b/frontend/packages/core/src/contexts/SystemMyServiceContext.tsx new file mode 100644 index 00000000..ffc4b34e --- /dev/null +++ b/frontend/packages/core/src/contexts/SystemMyServiceContext.tsx @@ -0,0 +1,23 @@ + +import { createContext, useContext, useState, ReactNode, FC } from 'react'; +import { MyServiceFieldType } from '../const/system/type.ts'; + +interface SystemMyServiceContextProps { + serviceInfo:MyServiceFieldType|undefined + setServiceInfo:React.Dispatch>; +} + +const SystemMyServiceContext = createContext(undefined); + +export const useSystemMyServiceContext = () => { + const context = useContext(SystemMyServiceContext); + if (!context) { + throw new Error('useArray must be used within a ArrayProvider'); + } + return context; +}; + +export const SystemMyServiceProvider: FC<{ children: ReactNode }> = ({ children }) => { + const [serviceInfo, setServiceInfo] = useState() + return {children}; +}; \ No newline at end of file diff --git a/frontend/packages/core/src/contexts/TeamContext.tsx b/frontend/packages/core/src/contexts/TeamContext.tsx new file mode 100644 index 00000000..f5ffd64d --- /dev/null +++ b/frontend/packages/core/src/contexts/TeamContext.tsx @@ -0,0 +1,24 @@ + +import { FC, createContext, useContext, useState, ReactNode } from 'react'; +import { TeamConfigType } from '../const/team/type'; + +interface TeamContextProps { + teamInfo?:TeamConfigType + setTeamInfo?: React.Dispatch>; +} + +const TeamContext = createContext(undefined); + +export const useTeamContext = () => { + const context = useContext(TeamContext); + if (!context) { + throw new Error('useArray must be used within a ArrayProvider'); + } + return context; +}; + +export const TeamProvider: FC<{ children: ReactNode }> = ({ children }) => { + const [teamInfo, setTeamInfo] = useState() + + return {children}; +}; \ No newline at end of file diff --git a/frontend/packages/core/src/index.css b/frontend/packages/core/src/index.css new file mode 100644 index 00000000..baf2543b --- /dev/null +++ b/frontend/packages/core/src/index.css @@ -0,0 +1,1201 @@ + +:root { + --layout-header-height: 72px; + --layout-footer-height: 30px; + + --btn-icon-margin: 5px; + /* //标签、输入框高度也用该变量 */ + --button-height: 32px; + --tabs-height: 43px; + --tree-item-height: 30px; + --collapse-header-height: 46px; + + --table-font-size: 14px; + --table-row-height: 40px; + --table-item-padding: 4px; + + --layout-sidebar-width: 90px; + --layout-sidebar-item-height: auto; + --icon-size: 16px; + --button-icon-margin: 5px; + + --MENU-BG-COLOR:#F7F8FA; + + --modal-mask-background-color: 0px 10px 15px 15px; + --border-radius: 4px; + --padding: 15px 20px; + --padding-x: 20px; + --padding-y: 15px; + + --margin: 20px; + + --menu-item-border-width: 3px; + + --modal-mask-background-color: 0px 10px 15px 15px; + --modal-mask-background-color: 0px 10px 15px 15px; + + /* //导航底部线条粗细 */ + --menu-item-border-width: 3px; + /* --padding: 20px; */ + --padding_X: 20px; + --padding_Y: 15px; + + --margin: 20px; + + --LAYOUT_PADDING: 12px; + /* y轴方向的距离 */ + --LAYOUT_MARGIN: 12px; + /* tabs之间的距离 */ + --TABS_MARGIN: 16px; + --PAGINATION_MARGIN_TOP: 16px; + + --icon-size: 16px; + --FORM_SPAN: 20px; + --button-icon-margin: 5px; + + /* 用于表单label中文字与星号距离、checkbox中文字与勾选框距离、树组件图标间距 */ + --small-padding: 4px; + --form-font-size: 14px; + + /* //###主题相关颜色### */ + --item-hover-background-color: #EBEEF2; + --modal-mask-background-color: rgba(0, 0, 0, 0.35); + --background-color: #fff; + --disabled-background-color: #f6f6f7; + --disabled-border-color: #dedede; + --disabled-text-color: #999999; + --divider-color: #f3f3f3; + /* /表单label中*颜色 */ + --required-symbol-color: #ff4d4f; + /* // 按钮 */ + /* //次要按钮 */ + --primary-hover-color: var(--item-hover-background-color); + --danger-color: red; + --disabled-background-color: #f3f3f3; + --text-color: #333333; + --text-hover-color: #333333cc; + /* --disabled-text-color: #cccccc; */ + --item-active-background-color: #f2f2f2; + --border-color: #ededed; + + --TITLE_TEXT: #666666; + --primary-color: #3D46F2; + --LABEL_COLOR: #5a5a5a; + --scrollbar-thumb-background-color: #EBEEF2; + --bar-background-color: #f7f8fa; + --button-shadow-color: #d9d9d9; + --selected-background-color: #f3f0ff; + + --collapse-border-color: var(--border-color); + + --select-border-color: var(--border-color); + --select-background-color: var(--background-color); + + --button-border-color: var(--border-color); + --button-shadow-color: var(--button-shadow-color); + --button-text-text-color: var(--text-color); + --button-text-hover-text-color: var(--text-color); + --button-text-hover-background-color: hsl(0, 0%, 95.1%); + --button-danger-text-color: var(--danger-color); + --button-primary-text-color_BLUE: var(--info-color); + --button-primary-text-color: #ffffff; + --button-primary-hover-text-color: #ffffff; + --button-primary-background-color: var(--primary-color); + --button-primary-hover-background-color: #7371fccc; + --button-primary-border-color: transparent; + --button-primary-hover-border-color: transparent; + --button-primary-border-color: rgba(0, 0, 0, 0.07); + --button-primary-active-border-color: #7371fccc; + --button-primary-active-background-color: #7371fccc; + --button-primary-shadow-color: var(--button-shadow-color); + --button-default-text-color: #000000; + --button-default-hover-text-color: #000000; + --button-default-background-color: #ffffff; + --button-default-hover-background-color: #ffffff; + --button-default-border-color: var(--border-color); + --button-default-hover-border-color: rgba(0, 0, 0, 0.07); + --button-default-hover-background-color: hsl(0, 0%, 95.1%); + --button-default-active-border-color: rgba(0, 0, 0, 0.07); + --button-default-active-background-color: hsl(0, 0%, 95.1%); + --button-default-shadow-color: var(--button-shadow-color); + --button-danger-text-color: #ff3c32; + --button-danger-border-color: #ff3c32; + --button-danger-background-color: #fff; + --button-danger-hover-text-color: rgba(255, 60, 50, 0.8); + --button-danger-hover-border-color: #ff3c32; + --button-danger-hover-background-color: #fff; + --button-danger-active-border-color: #ff3c32; + --button-danger-active-background-color: #fff; + --button-danger-shadow-color: var(--button-shadow-color); + --tabs-badge-color: var(--primary-color); + --tabs-active-badge-color: #3D46F2; + --tabs-background-color: var(--background-color); + --tabs-text-color: var(--text-color); + --tabs-active-color: var(--primary-color); + --tabs-active-text-color: var(--text-color); + --tabs-card-text-color: var(--text-color); + --tabs-card-background-color: #EBEEF2; + --tabs-card-item-background-color: var(--background-color); + --tabs-card-item-active-color: var(--primary-color); + --tabs-card-item-active-text-color: var(--text-color); + --tabs-card-item-active-background-color: var(--background-color); + --table-text-color: var(--text-color); + --table-border-color: rgba(0, 0, 0, 0.07); + --table-background-color: var(--background-color); + --table-header-text-color: var(--TITLE_TEXT); + --table-header-background-color: #EBEEF2; + --table-row-hover-background-color: hsl(0, 0%, 95.1%); + --table-footer-background-color: var(--background-color); + --table-footer-text-color: var(--text-color); + --tree-header-background-color: var(--background-color); + --tree-text-color: var(--text-color); + --tree-background-color: var(--background-color); + --tree-selected-text-color: var(--text-color); + --tree-selected-background-color: hsl(0, 0%, 95.1%); + --tree-hover-text-color: var(--text-color); + --tree-hover-background-color: hsl(0, 0%, 95.1%); + --dropdown-menu-background-color: var(--background-color); + --dropdown-item-text-color: var(--text-color); + --dropdown-item-hover-text-color: var(--text-color); + --dropdown-item-hover-background-color: hsl(0, 0%, 95.1%); + --menu-background-color: var(--background-color); + --menu-item-text-color: var(--text-color); + --menu-item-group-title-text-color: #999; + --menu-item-active-background-color: #EBEEF2; + --menu-item-active-color: var(--primary-color); + --menu-item-active-text-color: var(--text-color); + --menu-inline-submenu-background-color: #EBEEF2; + --select-text-color: var(--text-color); + --select-border-color: rgba(0, 0, 0, 0.07); + --select-hover-border-color: var(--primary-color); + --select-active-border-color: var(--primary-color); + --select-background-color: var(--background-color); + --select-dropdown-text-color: var(--text-color); + --select-dropdown-background-color: var(--background-color); + --select-item-selected-text-color: var(--text-color); + --select-item-selected-background-color: hsl(0, 0%, 95.1%); + --select-item-hover-background-color: hsl(0, 0%, 95.1%); + --input-text-color: var(--text-color); + --input-background-color: var(--background-color); + --input-icon-color: rgba(0, 0, 0, 0.25); + --input-border-color: rgba(0, 0, 0, 0.07); + --input-hover-border-color: var(--primary-color); + --input-active-border-color: var(--primary-color); + --input-placeholder-color: #bbbbbb; + --modal-header-background-color: var(--background-color); + --modal-header-text-color: var(--text-color); + --modal-body-background-color: var(--background-color); + --modal-body-text-color: var(--text-color); + --modal-footer-background-color: var(--background-color); + --modal-footer-text-color: var(--text-color); + --modal-mask-background-color: rgba(0, 0, 0, 0.30); + --pagination-item-background-color: var(--background-color); + --pagination-item-active-background-color: var(--background-color); + --pagination-button-background-color: var(--background-color); + --toast-success-text-color: #2ca641; + --toast-success-icon-color: #2ca641; + --toast-success-background-color: rgba(44, 166, 65, 0.1); + --toast-warning-icon-color: #ed6a0c; + --toast-warning-text-color: #ed6a0c; + --toast-warning-background-color: rgba(237, 106, 12, 0.1); + --toast-info-text-color: #2878ff; + --toast-info-icon-color: #2878ff; + --toast-info-background-color: rgba(40, 120, 255, 0.1); + --toast-error-text-color: #ff3c32; + --toast-error-icon-color: #ff3c32; + --toast-error-background-color: rgba(255, 60, 50, 0.1); + --alert-default-text-color: var(--text-color); + --alert-default-icon-color: var(--text-color); + --alert-default-background-color: rgba(149, 149, 149, 0.1); + --alert-success-text-color: #2ca641; + --alert-success-icon-color: #2ca641; + --alert-success-background-color: rgba(44, 166, 65, 0.1); + --alert-info-text-color: #2878ff; + --alert-info-icon-color: #2878ff; + --alert-info-background-color: rgba(40, 120, 255, 0.1); + --alert-error-text-color: #ff3c32; + --alert-error-icon-color: #ff3c32; + --alert-error-background-color: rgba(255, 60, 50, 0.1); + --alert-warning-text-color: #ed6a0c; + --alert-warning-icon-color: #ed6a0c; + --alert-warning-background-color: rgba(237, 106, 12, 0.1); + --checkbox-text-color: var(--text-color); + --checkbox-border-color: rgba(0, 0, 0, 0.07); + --checkbox-background-color: var(--background-color); + --checkbox-checked-background-color: var(--primary-color); + --checkbox-inner-color: var(--background-color); + --checkbox-checked-border-color: var(--primary-color); + --checkbox-checked-text-color: var(--text-color); + --radio-text-color: var(--text-color); + --radio-border-color: rgba(0, 0, 0, 0.07); + --radio-background-color: var(--background-color); + --radio-checked-text-color: var(--text-color); + --radio-checked-border-color: var(--primary-color); + --radio-checked-background-color: var(--primary-color); + --switch-active-color: var(--primary-color); + --switch-background-color: var(--background-color); + --switch-text-color: #fff; + --switch-card-border-color: rgba(0, 0, 0, 0.07); + --switch-card-background-color: var(--background-color); + --switch-card-text-color: var(--text-color); + --collapse-header-background-color: #EBEEF2; + --collapse-border-color: rgba(0, 0, 0, 0.07); + --collapse-content-background-color: var(--background-color); + --popover-background-color: rgba(0, 0, 0, 0.75); + --popover-text-color: #fff; + --progress-default-color: #2878ff; + --progress-success-color: #52c41a; + --progress-exception-color: #ff3c32; + --system-border-color: rgba(0, 0, 0, 0.07); + --text-secondary-color: #999; + --empty-simple-ellipse-background-color: #dce0e6; + --empty-simple-border-color: var(--TITLE_TEXT); + --empty-simple-box-background-color: #dce0e6; +} + +:root { + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: #333; + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500 ; + color: #646cff ; + text-decoration: inherit ; +} + +a:hover { + color:var(--button-primary-hover-background-color) ; +} + +body { + width:100vw; + height:100vh; + margin:0; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +/* button:hover { + border-color: #646cff; */ +/* } */ + +p{ + margin:0; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} + + +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb-background-color); + border-radius: 10px; +} +::-webkit-scrollbar-track { + background-color:transparent +} + +/* .hidden-switcher .ant-tree-switcher { + @apply hidden; +} */ +/* .ant-tree{ + font-size:14px !important; +} */ +/* .ant-tree.ant-tree-directory .ant-tree-treenode:hover:before{ + background: var(--menu-item-active-background-color) !important; +} +.ant-tree.ant-tree-directory .ant-tree-treenode-selected .ant-tree-switcher{ + color:#000 !important; +} */ + +.ant-table-cell { + a{ + color:var(--primary-color) !important; + } + + button.ant-btn.ant-btn-text{ + height:22px !important; + } + + button.ant-btn:not(:disabled):not(.text-table_text){ + color:var(--primary-color); + } + + button.ant-btn:not(:disabled):not(.text-table_text):hover{ + color:var(--button-primary-hover-background-color) !important; + } + +} +.ant-popover .ant-popover-inner{ + border-radius: 10px !important; + padding:10px !important; +} + +.ant-dropdown .ant-table-filter-dropdown{ + border-radius: 10px;; + /* .ant-dropdown-menu { */ + /* padding:4px 8px !important; */ + /* .ant-dropdown-menu-item-selected, + .ant-dropdown-menu-item:hover, + ant-dropdown-menu-item-selected:hover + { + background-color: transparent !important; + } + + .ant-dropdown-menu-item:hover .ant-checkbox-inner{ + border-color: var(--primary-color) !important; + } */ +/* } */ +} + +.ant-modal-wrap{ +.ant-modal.ant-modal-confirm-confirm{ + top:10% !important; + max-height:80vh !important; + padding-bottom: 0px !important; + .ant-modal-content{ + max-height: 80vh !important; + padding:0px !important; + .ant-modal-body{ + .ant-modal-confirm-body.ant-modal-confirm-body-has-title{ + max-height: 80vh !important; + .ant-modal-confirm-paragraph{ + row-gap: 0px !important; + max-height: 80vh !important; + max-width: 100% !important; + .ant-modal-confirm-title{ + padding:20px !important; + } + .ant-modal-confirm-content{ + max-height: calc(80vh - 136px) !important; + overflow: auto; + padding:0px 20px !important; + } + } + } + .ant-modal-confirm-btns{ + padding:20px !important; + margin:0px !important; + } + } + } + } +} + +.ant-modal-wrap.height-fixed-modal{ + .ant-modal.ant-modal-confirm-confirm{ + min-height: 540px !important; + .ant-modal-content{ + min-height: 540px !important; + .ant-modal-body{ + min-height: 540px !important; + .ant-modal-confirm-body.ant-modal-confirm-body-has-title{ + min-height: 468px !important; + .ant-modal-confirm-paragraph{ + min-height: 404px !important; + .ant-modal-confirm-content{ + min-height: 404px !important; + } + } + } + } + } + } +} + /* 弹窗最高高度90% */ +.ant-modal:not(.ant-modal-confirm-confirm){ + top:10% !important; + max-height:80vh !important; + padding-bottom: 0px !important; + /* >div:nth-child(2){ + max-height:100% !important; + display: flex; + flex:1 1 0%; + overflow-y: hidden; + } */ + .ant-modal-content{ + max-height: 80vh !important; + /* display: flex; + flex:1 1 0%; + justify-content: center; + flex-direction: column; + overflow-y: hidden;*/ + padding:0px !important; + .ant-modal-header{ + padding:20px!important; + margin:0px !important; + } + .ant-modal-body{ + max-height:calc(80vh - 136px); + /* width:100%; + display: flex; + flex: 1; */ + overflow: auto; + padding:0 20px; + .ant-modal-confirm-body-wrapper{ + /* max-height:100%; + overflow: hidden; + display: flex; + flex-direction: column; + flex: 1 1 0%; + flex-grow: 1; */ + .ant-modal-confirm-body{ + /* max-height:calc(100% - 44px); + height:calc(100% - 44px) !important; + max-width: unset !important; */ + .ant-modal-confirm-paragraph{ + /* max-height:100%; + height:100% !important; + max-width: unset; */ + row-gap:0px !important; + .ant-modal-confirm-title{ + padding:20px !important; + } + .ant-modal-confirm-content{ + overflow:auto; + padding:0 20px !important; + } + } + } + .ant-modal-confirm-btns{ + margin-top:0px !important; + padding:20px !important; + } + } + } + .ant-modal-footer { + margin-top:0px !important; + padding:20px !important; + } + } +} + + +.ant-modal-wrap.ant-modal-without-footer{ + .ant-modal{ + .ant-modal-content{ + .ant-modal-body{ + .ant-modal-confirm-body-wrapper{ + .ant-modal-confirm-body{ + /* max-height:100%; + height:100% !important; + max-width: unset !important; */ + } + } + } + } + } +} + +.ant-modal-wrap.modal-without-footer{ + /* 弹窗最高高度90% */ + .ant-modal:not(.ant-modal-confirm-confirm){ + top:10% !important; + max-height:80vh !important; + padding-bottom: 0px !important; + .ant-modal-content{ + max-height: 80vh !important; + padding:0px !important; + .ant-modal-header{ + padding:20px!important; + margin:0px !important; + } + .ant-modal-body{ + max-height:calc(80vh - 64px); + overflow: auto; + padding:0 20px; + .ant-modal-confirm-body-wrapper{ + .ant-modal-confirm-body{ + .ant-modal-confirm-paragraph{ + row-gap:0px !important; + .ant-modal-confirm-title{ + padding:20px !important; + } + .ant-modal-confirm-content{ + overflow:auto; + padding:0 20px !important; + } + } + } + .ant-modal-confirm-btns{ + margin-top:0px !important; + padding:20px !important; + } + } + } + } + } +} + +.ant-modal-wrap.height-fixed-modal{ + .ant-modal:not(.ant-modal-confirm-confirm){ + min-height: 540px !important; + .ant-modal-content{ + min-height: 540px !important; + .ant-modal-body{ + min-height: 404px !important; + /* .ant-modal-confirm-content{ + min-height: 404px !important; + } */ + } + } + } +} + +.ant-spin-container{ + height:100%; +} +/* loading时表格不显示empty */ +.ant-spin-container.ant-spin-blur .ant-table .ant-empty.ant-empty-normal{ + opacity: 0; +} + +/* 表格中的复制按钮不常显,鼠标悬浮至单元格时才显示 */ +.ant-table-cell .ant-typography-copy{ + opacity: 0; +} + +.ant-table-cell:hover .ant-typography-copy{ + opacity: 1; +} + +.ant-typography .ant-typography-copy{ + color:var(--primary-color) !important; +} + + +.ant-typography .ant-typography-copy:hover{ + color:var(--button-primary-hover-background-color) !important; +} + + +.ant-typography .ant-typography-edit{ + display: none !important; +} + +.ant-typography:hover .ant-typography-edit{ + color:var(--text-color) !important; + display: inline-block !important; +} + +/* 隐藏tinymce的logo */ +.tox-statusbar__branding{ + display:none +} + +.inside-page-tabs.ant-tabs{ + > .ant-tabs-nav{ + margin:0px !important; + } + >.ant-tabs-nav::before{ + display: none !important; + } + } + +/* 卡片式Tabs样式调整 */ +.ant-tabs-card{ + >.ant-tabs-nav::before{ + border-bottom:1px solid var(--bar-background-color) !important; + } + >.ant-tabs-nav .ant-tabs-nav-wrap{ + background-color:var(--bar-background-color) !important ; + + .ant-tabs-tab{ + height:42px; + border:none !important; + border-radius: 0px !important; + transition:none; + } + .ant-tabs-tab.ant-tabs-tab-active{ + border-top:2px solid var(--primary-color) !important; + border-bottom: 1px solid var(--bar-background-color) !important; + } + } +} + +.MuiDataGrid-main > div:last-child:not(.MuiDataGrid-virtualScroller){ + display: none; +} + +.ant-pro-table-list-toolbar-setting-items{ + position:absolute; + top:13px; + right:16px; + z-index:9; + .ant-pro-table-list-toolbar-setting-item{ + font-size:14px; + color:var(--button-text-text-color); + } +} + +.ant-pro-table-column-setting-list-item-title{ + max-width: unset !important; + .ant-typography-ellipsis.ant-typography-single-line{ + width: unset !important; + } +} + +.ant-popover.ant-pro-table-column-setting-overlay{ + .ant-popover-inner-content{ + width:170px; + } +} + +.not-top-border-table { + th.ant-table-cell{ + border-top:0px !important; + } +} + +.not-top-padding-table{ + .ant-pro-table .ant-pro-table-list-toolbar-container{ + padding-block:0px !important; + padding:0px !important; + } + .ant-pro-table-list-toolbar .ant-pro-table-list-toolbar-left{ + margin-block-end: 0px !important; + } +} + +.ant-drawer{ + .ant-drawer-header{ + padding:20px !important; + border-bottom: none !important; + .ant-drawer-header-title{ + flex-direction: row-reverse; + .ant-drawer-title{ + color:var(--text-color) !important; + font-weight:bold !important; + .ant-radio-group{ + font-weight: normal !important; + .ant-radio-button-checked{ + font-weight: bold !important; + } + } + } + .ant-drawer-close{ + color:var(--text-color) !important; + padding:0px !important; + margin-inline-end:0px !important; + } + } + } + + .ant-drawer-body{ + padding: 0px 20px !important; + .eo_page_list .ant-pro-table-list-toolbar-container{ + padding-left:0 !important; + padding-top:0 !important; + } + } + + .ant-drawer-footer{ + padding:16px 20px !important + } +} + +.g6-tooltip { + padding: 10px 6px; + color: #444; + background-color: rgba(255, 255, 255, 0.9); + border: 1px solid #e2e2e2; + border-radius: 4px; +} + +.ant-pro-card .ant-pro-card-body{ + padding-inline:0 !important; +} + +.ant-table-tbody-virtual-scrollbar-thumb, .rc-virtual-list-scrollbar-thumb{ + background-color: var(--scrollbar-thumb-background-color) !important; +} + +.rc-virtual-list-scrollbar-horizontal,.ant-table-tbody-virtual-scrollbar-horizontal{ + height:10px !important; +} + +/* 生产环境无法获取到下列样式,先写在这里 */ +.eo_page_list .ant-pro-card .ant-pro-card-body{ + padding:0 !important; +} + .eo_page_list .ant-pro-table-list-toolbar-container{ + padding:12px 20px 12px 12px !important; + + .ant-pro-table-list-toolbar-right{ + flex:unset !important; + justify-content: flex-start; + flex-direction: row-reverse; + } + + .ant-input-group-addon .ant-input-search-button{ + display:none; + } +} + +.ant-table-filter-column{ + align-items: center; + +} +.ant-table-wrapper .ant-table-filter-trigger{ + padding:0px 5px !important; + margin-inline: 4px -4px !important; + height:26px !important; +} + +.anticon.anticon-caret-down.ant-table-column-sorter-down, +.anticon.anticon-caret-up.ant-table-column-sorter-up{ + height:11px !important; + width:11px !important; +} + .eo_page_list .ant-table-wrapper { + .ant-table-pagination.ant-pagination { + margin: 1px 10px 0 !important; + padding: 10px 0; + /* box-shadow: 0 -2px 2px -2px var(--border-color); */ + } + .ant-table.ant-table-middle{ + .ant-table-thead>tr>th, + .ant-table-thead>tr>td{ + border-top:1px solid var(--border-color); + border-bottom:1px solid var(--border-color); + } + + + .ant-table-thead>tr>th{ + color:#666666; + } + } + +} + +/* .ant-tree .ant-tree-treenode{ + padding:0 !important; + .ant-tree-switcher{ + width:34px !important; + } +} + +.ant-tree.ant-tree-directory .ant-tree-treenode:before{ + bottom:0px !important; +} */ + +.ant-tree.ant-tree-directory .ant-tree-treenode:hover:before{ + background-color: #EBEEF2; +} + +.ant-tree .ant-tree-node-content-wrapper{ + display:flex; + flex-wrap:nowrap; + overflow:hidden; +} + +.icon-tree{ + .ant-tree-title{ + overflow:hidden; + width:calc(100% - 24px) + } +} + +/* .ant-dropdown .ant-dropdown-menu{ + border-radius: 10px; + padding: 10px; + li.ant-dropdown-menu-item{ + padding:0 !important; + >button.ant-btn{ + width:100%; + padding:10px 12px !important; + height:32px !important; + } + >span.ant-dropdown-menu-title-content{ + padding:0px !important; + min-width: 80px; + /* padding:0px 12px !important; + height:32px !important; + line-height: 32px !important; + .ant-btn{ + width:100%; + padding:0px 12px !important; + } + .ant-btn-default:not(:disabled):not(.ant-btn-disabled):hover{ + color:var(--text-color) !important; + } + } + } + +} */ +.ant-dropdown-button{ + .ant-btn-link{ + color:var(--text-color) !important; + } + + .ant-btn-link:not(:disabled):not(.ant-btn-disabled):hover{ + color:var(--text-color) !important; + } +} + +.ant-space-compact-block.ant-dropdown-button:hover{ + .ant-btn-link{ + color:var(--text-hover-color) !important; + } + + .ant-btn-link:not(:disabled):not(.ant-btn-disabled):hover{ + color:var(--text-hover-color) !important; + } +} +.ant-divider-horizontal{ + margin:12px 0 !important; +} + +.small-tabs.ant-tabs .ant-tabs-nav{ + margin-bottom:12px; + .ant-tabs-tab{ + padding:10px 8px; + } +} + +.drawer-title-group{ + .ant-radio-button-wrapper{ + border:none !important; + padding-inline:6px !important; + font-size:14px; + } + .ant-radio-button-wrapper:first-child{ + padding-left:0px !important; + } + .ant-radio-button-wrapper:not(:first-child)::before{ + display: none; + } + .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled){ + color:var(--text-color) !important; + font-weight: bold; + } +} + +/* .ant-menu-submenu{ + > div.ant-menu-submenu-title{ + color:#999999 !important; + height:36px !important; + padding-left:16px !important; + >span{ + height:36px !important; + } + } +} + +.ant-menu-inline .ant-menu-submenu-title{ + padding-inline-end: 20px !important; +} */ + +.ant-formily-array-table{ + width:568px; +} +.ant-formily-item-label.ant-formily-item-item-col-6{ + flex: 0 0 16.666666666666664% !important; + max-width: 16.666666666666664% !important; + justify-content: flex-start !important; +} + +.ant-formily-item-control.ant-formily-item-item-col-10{ + flex: 0 0 83.33333333333334% !important; + max-width: 83.33333333333334% !important; +} + +.ant-list-header{ + padding:8px 0 !important; +} +.ant-drawer-content-wrapper{ + min-width: 820px !important; + .ant-table:not(.ant-table-bordered){ + border:1px solid var(--border-color) !important; + border-top:0px !important; + } + .ant-table:not(.ant-table-borderer){ + border-bottom: 0px !important; + } +} + +.apipark-layout-layout .ant-drawer-content-wrapper{ + min-width:unset !important; +} + +.table-border { + .ant-table:not(.ant-table-bordered){ + border:1px solid var(--border-color) !important; + border-top:0px !important; + } + .ant-table:not(.ant-table-borderer){ + border-bottom: 0px !important; + } + +} + +.ant-modal-wrap:not(.height-fixed-modal){ +.ant-modal-body{ + .ant-table:not(.ant-table-bordered){ + border:1px solid var(--border-color) !important; + border-top:0px !important; + } + .ant-table:not(.ant-table-borderer){ + border-bottom: 0px !important; + } +} +} + + +.ant-tooltip{ + max-width: 280px !important; + .ant-tooltip-content .ant-tooltip-inner{ + word-break:break-all; + } +} + +.ant-form-item-extra{ + color:var(--disabled-text-color) !important; + font-size: 12px !important; +} + +.ant-form-item-explain{ + font-size: 12px !important; +} +.ant-form:not(.no-bg-form) .ant-form-item { + .ant-input-number.ant-input-number-in-form-item{ + width:100% !important; + } + background-color: #fcfcfc; + padding: 10px; + border-radius: 10px; + border: 1px solid #f2f2f2; +} + +.ant-form-item-margin-offset{ + margin-bottom:0px !important; +} + +.ant-select-focused.ant-select-outlined:not(.ant-select-disabled):not(.ant-select-customize-input):not(.ant-pagination-size-changer) .ant-select-selector{ + box-shadow: none !important; +} + +.ant-breadcrumb { + li{ + height: 32px !important; + line-height: 32px !important; + span{ + display: block; + height: 32px !important; + line-height: 32px !important; + a{ + display: block; + height: 32px !important; + line-height: 32px !important; + padding:0 12px !important; + border-radius: 4px !important; + } + a:hover{ + background-color: var(--item-hover-background-color) !important; + } + } + } +} + +.anticon{ + height:16px !important; + width: 16px !important; + justify-content:center; +} + +.ant-table-wrapper .ant-table{ + scrollbar-color: none !important; +} + +.eo_page_drag .ant-table-body{ + overflow-y: auto !important; +} +.transfer-table-member, +.transfer-table-api{ + .ant-table-wrapper .ant-table-thead >tr>th, + .ant-table-wrapper .ant-table-thead >tr>td{ + background-color: var(--MAIN_BG); + border:none; + } + + .ant-table-wrapper .ant-table-tbody-virtual .ant-table-cell{ + border:none; + } + + .rc-virtual-list-scrollbar.rc-virtual-list-scrollbar-horizontal, + .ant-table-tbody-virtual-scrollbar.ant-table-tbody-virtual-scrollbar-horizontal{ + display: none; + } + + .ant-table-wrapper .ant-table-tbody .ant-table-row > .ant-table-cell-row-hover{ + background: #EBEEF2; + } + + .ant-table-wrapper .ant-table-tbody .ant-table-row.ant-table-row-selected > .ant-table-cell-row-hover{ + background: #EBEEF2; + } + .ant-table-thead >tr>th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before{ + display: none; + } +} + +.ant-alert-info{ + background: #1784FC1A !important; +} + +.monaco-editor .find-widget .monaco-inputbox.synthetic-focus{ + outline-color: var(--primary-color) !important; +} + +.ant-cascader-dropdown .ant-cascader-menu-item:hover{ + background:#EBEEF2 !important; +} + +.ant-select-dropdown .ant-select-item-option-active:not(.ant-select-item-option-disabled){ + background-color: #EBEEF2 !important; +} + +.ant-divider-vertical{ + border-inline-start: 1px solid #EDEDED !important; + border-block-start:1px solid #EDEDED !important; +} + +.ant-input-prefix .anticon{ + color:#BBB !important; +} + + +.ant-collapse>.ant-collapse-item >.ant-collapse-header .ant-collapse-expand-icon{ + padding-inline-end:8px !important; +} + +.service-hub-description .ant-descriptions-item-container{ + .ant-descriptions-item-label{ + color: #333 !important; + } + .ant-descriptions-item-content{ + color:#999 !important; + } + +} + +.tree-transfer{ + .ant-transfer-list-header{ + display: none !important; + } +} + +.ant-tree-title{ + width:100%; +} + +.ant-pro-table-list-toolbar-title{ + flex-wrap:wrap !important; + row-gap:12px; +} +.ace-xcode .ace_print-margin{ + display:none !important; +} + +.no-first-switch-tree .ant-tree-switcher{ + display: none; +} + +.no-selected-tree .ant-tree-node-selected{ + background-color: transparent !important; +} + +.ant-drawer .ant-drawer-mask, +.ant-modal-root .ant-modal-mask{ + background-attachment: var(--modal-mask-background-color); + /* backdrop-filter: blur(2px); */ +} + +.ant-btn{ + height:35px !important; +} + +/* 1.顶部不能增加padding, virtuoso顶部增加padding时,滚动条会闪烁 ; + 2. 底部padding为60时,实际展示的才是40px*/ +.padding-top-40 .virtuoso-grid-list{ + padding-bottom:60px !important; +} + +/* .padding-top-20 .virtuoso-grid-list{ + padding-bottom:60px !important; + padding:20px 40px 40px !important; +} */ + +.ant-form-item-control-input-content{ + .ant-pro-card-body{ + padding-bottom: 0px !important; + } +} + +.ant-drawer-content.full-tabs{ + .ant-drawer-body{ + padding:0px !important; + .ant-tabs.h-full.full-tabs { + .ant-tabs-content-holder{ + overflow: auto; + padding:0 20px; + .ant-tabs-content.ant-tabs-content-top{ + height:100%; + .ant-tabs-tabpane.ant-tabs-tabpane-active{ + height:100%; + } + } + } + } + } +} + +.ant-form-item-label, +.ant-formily-item-label{ + font-weight: bold; +} + +/* .eo_page_list.role_table .ant-pro-table-list-toolbar-container{ + padding-left:0 !important; + padding-right:0 !important; +} */ \ No newline at end of file diff --git a/frontend/packages/core/src/main.tsx b/frontend/packages/core/src/main.tsx new file mode 100644 index 00000000..4b3f3e92 --- /dev/null +++ b/frontend/packages/core/src/main.tsx @@ -0,0 +1,28 @@ + +import {StrictMode} from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.tsx' +import './index.css' +import {GlobalProvider} from "@common/contexts/GlobalStateContext.tsx"; + +async function initializeApp() { + try { + // 初始化行为 + // await fetchInitialConfig(); // 示例:获取初始配置 + + // 异步操作完成后,渲染React应用 + ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + , + ); + } catch (error) { + console.error('Initialization failed:', error); + // 处理初始化失败的情况,比如渲染一个错误界面 + } +} + +// 执行初始化 +initializeApp(); \ No newline at end of file diff --git a/frontend/packages/core/src/pages/Login.tsx b/frontend/packages/core/src/pages/Login.tsx new file mode 100644 index 00000000..1f973686 --- /dev/null +++ b/frontend/packages/core/src/pages/Login.tsx @@ -0,0 +1,157 @@ +import {FC, useCallback, useEffect, useRef, useState} from "react"; +import {App, Button, Form, FormInstance, Input} from "antd"; +import {useGlobalContext} from "@common/contexts/GlobalStateContext.tsx"; +import {useFetch} from "@common/hooks/http.ts"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useNavigate} from "react-router-dom"; +// import {useCrypto} from "../hooks/crypto.ts"; +import Logo from '@common/assets/logo.png' + +const Login:FC = ()=> { + const {state, dispatch} = useGlobalContext() + const {fetchData} = useFetch() + const { message } = App.useApp() + const navigate = useNavigate(); + const formRef = useRef(null); + const [loading,setLoading] = useState() + // const { encryptByEnAES } = useCrypto(); + + + const check = useCallback(()=>{ + fetchData, status:string}>>('account/login',{method:'GET'}).then(response=>{ + const {code,data} = response + if(code === STATUS_CODE.SUCCESS && data.status !== 'anonymous'){ + dispatch({type:'LOGIN'}) + navigate(state.mainPage) + }else{ + dispatch({type:'LOGOUT'}) + } + }) + },[]) + + + const getSystemInfo = useCallback(()=>{ + fetchData>('common/version',{method:'GET', eoTransformKeys:['build_time']}).then(response=>{ + const {code,data} = response + if(code === STATUS_CODE.SUCCESS){ + dispatch({type:'UPDATE_VERSION',version:data.version}) + dispatch({type:'UPDATE_DATE',updateDate:data.buildTime}) + } + }) + },[]) + + const login = async () => { + if (formRef.current) { + try { + const values = await formRef.current.validateFields(); + setLoading(true); + + const { username, password } = values; + // const encryptedPassword = encryptByEnAES(username, password); + + const body = { + name:username, + password: password + // client: 1, + // type: 1, + // app_type: 4, + }; + + const {code,msg } = await fetchData>('account/login/username',{method:'POST',eoBody:(body)}) + + if (code === STATUS_CODE.SUCCESS) { + dispatch({type:'LOGIN'}) + message.success('登录成功'); + const callbackUrl = new URLSearchParams(window.location.search).get('callbackUrl'); + if (callbackUrl && callbackUrl !== 'null') { + navigate(callbackUrl); + } else { + navigate(state.mainPage); + } + }else{ + dispatch({type:'LOGOUT'}) + message.error(msg) + } + + } catch (err) { + console.warn(err); + } finally { + setLoading(false) + } + } + }; + + useEffect(() => { + check() + getSystemInfo() + }, []); + + return ( +
+
+
+ + + +
+ +
+
+
+
+ 登录 +
+ +
+ + + + + + + + +
+ + + +
+ +
+
+
+ +
+

Version {state.version}-{state.updateDate}

+

{state.powered}

+
+
+
+ ); +} +export default Login; \ No newline at end of file diff --git a/frontend/packages/core/src/pages/approval/ApprovalList.tsx b/frontend/packages/core/src/pages/approval/ApprovalList.tsx new file mode 100644 index 00000000..73cebe2c --- /dev/null +++ b/frontend/packages/core/src/pages/approval/ApprovalList.tsx @@ -0,0 +1,181 @@ +import {ActionType, ProColumns} from "@ant-design/pro-components"; +import {App, Button} from "antd"; +import {useEffect, useMemo, useRef, useState} from "react"; +import PageList from "@common/components/aoplatform/PageList.tsx"; +import { + PUBLISH_APPROVAL_TABLE_COLUMN, + SUBSCRIBE_APPROVAL_TABLE_COLUMN, + TODO_LIST_COLUMN_NOT_INCLUDE_KEY +} from "@common/const/approval/const.tsx"; +import { + ApprovalTableListItem, + PublishApprovalInfoType, + SubscribeApprovalInfoType, +} from "@common/const/approval/type.tsx"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import { + SubscribeApprovalModalContent, + SubscribeApprovalModalHandle +} from "@common/components/aoplatform/SubscribeApprovalModalContent.tsx"; +import { + PublishApprovalModalContent, + PublishApprovalModalHandle +} from "@common/components/aoplatform/PublishApprovalModalContent.tsx"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import { SimpleMemberItem } from "@common/const/type.ts"; +import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx"; + +export default function ApprovalList({pageType,pageStatus}:{pageType:'subscribe'|'release',pageStatus:0|1}){ + const { modal,message } = App.useApp() + const [searchWord, setSearchWord] = useState('') + // const [confirmLoading, setConfirmLoading] = useState(false); + const pageListRef = useRef(null); + const [init, setInit] = useState(true) + const {fetchData} = useFetch() + const [tableHttpReload, setTableHttpReload] = useState(true); + const [tableListDataSource, setTableListDataSource] = useState([]); + const subscribeRef = useRef(null) + const publishRef = useRef(null) + const [approvalBtnLoading,setApprovalBtnLoading] = useState(false) + const [memberValueEnum, setMemberValueEnum] = useState<{[k:string]:{text:string}}>({}) + + const getApprovalList = ()=>{ + if(!tableHttpReload){ + setTableHttpReload(true) + return Promise.resolve({ + data: tableListDataSource, + success: true, + }); + } + return fetchData>( `approval/${pageType}s`,{method:'GET',eoParams:{keyword:searchWord,status:pageStatus},eoTransformKeys:['apply_time','apply_project','approval_time']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setTableListDataSource(data.approvals) + !init && message.success(msg || '操作成功') + setInit((prev)=>prev ? false : prev) + return {data:data.approvals, success: true} + }else{ + message.error(msg || '操作失败') + return {data:[], success:false} + } + }).catch(() => { + return {data:[], success:false} + }) + } + + useEffect(()=>{ + getMemberList() + },[]) + + useEffect(() => { + getApprovalList(); + }, [pageType,pageStatus]); + + const openModal = async(type:'approval'|'view',entity:ApprovalTableListItem)=>{ + message.loading('正在加载数据') + const {code,data,msg} = await fetchData>(`approval/${pageType}`,{method:'GET',eoParams:{id:entity!.id},eoTransformKeys:['apply_project','apply_team','apply_time','approval_time']}) + message.destroy() + if(code === STATUS_CODE.SUCCESS){ + const modalInst = modal.confirm({ + title:type === 'approval' ? '审批' : '查看', + content:pageType === 'subscribe' ? + + :, + onOk:()=>{ + if(type === 'approval'){ + return (pageType === 'subscribe'? subscribeRef.current?.save('pass') : publishRef.current?.save('pass'))?.then((res)=> { + res === true && manualReloadTable + }) + } + }, + width:600, + okText:type === 'approval' ? '通过' :'确认', + cancelText:'取消', + closable:true, + onCancel:()=>{setApprovalBtnLoading(false)}, + icon:<>, + footer:(_, { OkBtn, CancelBtn }) =>{ + return ( + <> + {type === 'approval' ? <> + + + + : + <> + + + } + + ) + }, + }) + }else{ + message.error(msg || '操作失败') + return + } + } + + + const getMemberList = async ()=>{ + setMemberValueEnum({}) + const {code,data,msg} = await fetchData>('simple/member',{method:'GET'}) + if(code === STATUS_CODE.SUCCESS){ + const tmpValueEnum:{[k:string]:{text:string}} = {} + data.members?.forEach((x:SimpleMemberItem)=>{ + tmpValueEnum[x.name] = {text:x.name} + }) + setMemberValueEnum(tmpValueEnum) + }else{ + message.error(msg || '操作失败') + } + } + + const operation:ProColumns[] =[ + { + title: '操作', + key: 'option', + width: 62, + fixed:'right', + valueType: 'option', + render: (_: React.ReactNode, entity: ApprovalTableListItem) => [ + pageStatus === 0 ? + {openModal('approval',entity)}} btnTitle="审批"/> + :{openModal('view',entity)}} btnTitle="查看"/>, + ] + } + ] + + const columns = useMemo(()=>{ + const newCol = [...(pageType === 'subscribe'? SUBSCRIBE_APPROVAL_TABLE_COLUMN:PUBLISH_APPROVAL_TABLE_COLUMN)] + const res = pageStatus === 0 ? newCol.filter((x)=>TODO_LIST_COLUMN_NOT_INCLUDE_KEY.indexOf(x.dataIndex as string) === -1): newCol + return res + },[pageType,pageStatus]) + + + const manualReloadTable = () => { + setTableHttpReload(true); // 表格数据需要从后端接口获取 + pageListRef.current?.reload() + }; + + return ( +
+ getApprovalList(sorter as { [k: string]: string; } | undefined)} + dataSource={tableListDataSource} + columns = {[...columns,...operation]} + searchPlaceholder="输入申请人、服务、团队查找" + onSearchWordChange={(e) => { + setSearchWord(e.target.value) + }} + onChange={() => { + setTableHttpReload(false) + }} + onRowClick={(row:ApprovalTableListItem)=>openModal(pageStatus === 0 ? 'approval': 'view',row)} + /> +
+ ) +} \ No newline at end of file diff --git a/frontend/packages/core/src/pages/approval/ApprovalPage.tsx b/frontend/packages/core/src/pages/approval/ApprovalPage.tsx new file mode 100644 index 00000000..0e619bd2 --- /dev/null +++ b/frontend/packages/core/src/pages/approval/ApprovalPage.tsx @@ -0,0 +1,70 @@ + +import { Menu, MenuProps, Tabs, TabsProps} from "antd"; +import {Link, useLocation, useNavigate} from "react-router-dom"; +import {useEffect, useState} from "react"; +import ApprovalList from "./ApprovalList.tsx"; +import { getItem } from "@common/utils/navigation.tsx"; + + +const menuItems: MenuProps['items'] = [ + getItem('管理', 'mng', null, + [ + getItem(订阅申请, 'subscribe'), + getItem(发布申请, 'release')], + 'group'), +]; + +const items: TabsProps['items'] = [ + { + key: '0', + label: '待审批', + }, + { + key: '1', + label: '已审批', + } +]; + +export default function ApprovalPage(){ + const navigateTo = useNavigate() + const location = useLocation() + const currentUrl = location.pathname + const query =new URLSearchParams(useLocation().search) + const [pageType,setPageType] = useState<'subscribe'|'release'>((query?.get('type') ||'subscribe') as 'subscribe'|'release') + const [pageStatus,setPageStatus] = useState<0|1>(Number(query.get('status') ||0) as 0|1) + + const onMenuClick: MenuProps['onClick'] = (e) => { + setPageType(e.key as 'subscribe'|'release') + navigateTo(`${currentUrl}?type=${e.key}&status=${pageStatus}`); + }; + + const onChange = (key:string ) => { + setPageStatus(Number(key) as 0|1) + navigateTo(`${currentUrl}?type=${pageType}&status=${key}`); + }; + + + useEffect(() => { + setPageType((query?.get('type') ||'subscribe') as 'subscribe'|'release') + setPageStatus(Number(query.get('status') ||0) as 0|1) + }, [currentUrl]); + + return ( + <> +
+ +
+ + +
+
+ + ) +} \ No newline at end of file diff --git a/frontend/packages/core/src/pages/auditLog/AuditLog.module.css b/frontend/packages/core/src/pages/auditLog/AuditLog.module.css new file mode 100644 index 00000000..f5f7edf5 --- /dev/null +++ b/frontend/packages/core/src/pages/auditLog/AuditLog.module.css @@ -0,0 +1,8 @@ +.audit-log-table{ + :global .ant-pro-table-search { + margin-block-end: 0px; + } + :global .ant-pro-query-filter.ant-pro-query-filter { + padding: 10px; + } +} \ No newline at end of file diff --git a/frontend/packages/core/src/pages/auditLog/AuditLog.tsx b/frontend/packages/core/src/pages/auditLog/AuditLog.tsx new file mode 100644 index 00000000..62fc17e6 --- /dev/null +++ b/frontend/packages/core/src/pages/auditLog/AuditLog.tsx @@ -0,0 +1,309 @@ +import {ProColumns, ProProvider, ProTable} from "@ant-design/pro-components"; +import {FC, useContext, useEffect, useRef, useState} from "react"; +import {App, Button, Select, Space} from "antd"; +import {debounce} from "lodash-es"; +import styles from './AuditLog.module.css' +import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import {SortOrder} from "antd/es/table/interface"; +import {DefaultOptionType} from "antd/es/cascader"; +import moment from "moment"; +import { SimpleMemberItem } from "@common/const/type.ts"; + +type AuditLogTableListItem = { + operator:string; + operateType:string; + description:string; + ip:string; + operateTime:string +}; + +const searchTypeList = [{label:'包含',value:1},{label:'不包含',value:0}]; + +const AUDIT_LOG_COLUMNS_CONFIG: ProColumns[] = [ + { + title: '操作时间', + dataIndex: 'operateTime', + valueType: 'dateTimeRange', + order:1, + copyable: true, + ellipsis:true, + fixed:'left', + width:182 + }, + { + title: '操作人', + dataIndex: ['operator','name'], + key: 'operator', + order:3, + valueType: 'multipleSelect' + }, + { + title: '操作类型', + dataIndex: 'operateType', + key: 'operateType', + order:2, + valueType: 'multipleSelect', + }, + { + title: '具体描述', + dataIndex: 'description', + key: 'description', + search:false + }, + { + title: 'IP', + dataIndex: 'ip', + key: 'ip', + search:false + } +]; + + + +const MultipleSelect: FC<{ + state: { + type: number; + fieldName:string; + options:DefaultOptionType[] + }; + /** Value 和 onChange 会被自动注入 */ + value?: string; + onChange?: (value: {[k:string]:unknown},) => void; +}> = (props) => { + const { state,value,onChange } = props; + + const [innerOptions, setOptions] = useState(state.options); + + const [searchType,setSearchType]=useState<1|0>(1) + const [searchValue,setSearchValue]=useState(null) + useEffect(() => { + setOptions(state.options); + }, [state.options]); + + const handleSearchTypeChange = (e:1|0)=>{setSearchType(e);onChange?.({include:searchType, value:searchValue})} + const handleSearchValueChange = (e:string|number|null)=>{ + //console.log(e) + setSearchValue(e); + onChange?.({include:searchType, value:e})} + + return ( + + + + ); +}; + + +export default function AuditLog(){ + const { message } = App.useApp() + const values = useContext(ProProvider); + const parentRef = useRef(null); + const [tableHeight, setTableHeight] = useState(window.innerHeight); + const { setBreadcrumb } = useBreadcrumb() + const [tableListDataSource, setTableListDataSource] = useState([]); + const {fetchData} = useFetch() + const [columns, setColumns] = useState[]>(AUDIT_LOG_COLUMNS_CONFIG) + const [operatorTypeList, setOperatorTypeList] = useState([]) + const [operatorList, setOperatorList] = useState([]) + + const getOperatorTypeList = async ()=>{ + setOperatorTypeList([]) + const {code,data,msg} = await fetchData}>>('audit/operate_types',{method:'GET',eoTransformKeys:['operate_types']}) + if(code === STATUS_CODE.SUCCESS){ + setOperatorTypeList(data.operateTypes?.map((x:{id:string,name:string})=>({label:x.name, value:x.id}))) + }else{ + message.error(msg || '操作失败') + } + } + + const getMemberList = async ()=>{ + setOperatorList([]) + const {code,data,msg} = await fetchData>('simple/member',{method:'GET'}) + if(code === STATUS_CODE.SUCCESS){ + setOperatorList(data.members?.map((x:SimpleMemberItem)=>{return { + label:x.name, value:x.id + }})) + }else{ + message.error(msg || '操作失败') + } + } + + const handleOperatorList = ()=>{ + setColumns((prevData)=> + prevData?.map((x)=>{ + if(x.dataIndex === 'operator'){ + x.renderFormItem = (item, { type, defaultRender, ...rest }, form) => { + const stateType = form.getFieldValue('operator'); + return ( + + ); + } + } + return x + })) + } + + const handleOperatorTypeList = async ()=>{ + setColumns((prevData)=> + prevData?.map((x)=>{ + if(x.dataIndex === 'operateType'){ + x.renderFormItem= (item, { type, defaultRender, ...rest }, form) => { + const stateType = form.getFieldValue('operator'); + return ( + + ); + } + } + return x + })) + } + + useEffect(() => { + setBreadcrumb([ + { + title:'审计日志' + }, + ]) + const handleResize = () => { + if (parentRef.current) { + const res = parentRef.current.getBoundingClientRect(); + const height = res.height - 52 - 40;// 减去顶部按钮、底部分页、表头高度 + //console.log(height, res?.height); + height && setTableHeight(height); + } + }; + const debouncedHandleResize = debounce(handleResize, 200); + const resizeObserver = new ResizeObserver(debouncedHandleResize); + if (parentRef.current) { + resizeObserver.observe(parentRef.current); + } + getOperatorTypeList().then(()=>{ + handleOperatorTypeList() + }) + getMemberList().then(()=>{ + handleOperatorList() + }) + return () => { + resizeObserver.disconnect(); + }; + }, []); + + const getAuditLogList =(params:unknown, sorter?:Record,filter?:Record): Promise<{ data: AuditLogTableListItem[], success: boolean }>=> { + const eoParams = { + ...(params.operateTime?.length > 0 ? { + startTime:moment(params.operateTime[0],'YYYY-MM-DD HH:mm:ss').valueOf() / 1000, + endTime:moment(params.operateTime[0],'YYYY-MM-DD HH:mm:ss').valueOf() / 1000 + }:{}), + ...(params.operator?.value?.length > 0 ? { + operator:JSON.stringify(params.operator.include ? params.operator.value :operatorList.filter(item=>!params.operator.value.includes(item.value))?.map(x=>x.value)) + }:{}), + ...(params.operateType?.value?.length > 0 ? { + operateType:JSON.stringify(params.operateType.include ? params.operateType.value :operatorTypeList.filter(item=>!params.operatorTypeList.value.includes(item.value))?.map(x=>x.value)) + }:{}), + } + return fetchData>('audit/logs',{method:'GET',eoParams,eoTransformKeys:['startTime','endTime','operateType','operate_type','operate_time']}).then(response=>{ + const {code,data,msg} = response + //console.log(code,data.items) + if(code === STATUS_CODE.SUCCESS){ + setTableListDataSource(data.items) + return {data:data.items, success: true} + }else{ + message.error(msg || '操作失败') + return {data:[], success:false} + } + }).catch(() => { + return {data:[], success:false} + }) + } + const getOutputLog = (params:unknown)=>{ + const eoParams = { + ...(params.operateTime?.length > 0 ? { + startTime:moment(params.operateTime[0]).valueOf() / 1000, + endTime:moment(params.operateTime[0]).valueOf() / 1000 + }:{}), + ...(params.operator?.value?.length > 0 ? { + operator:JSON.stringify(params.operator.include ? params.operator.value :operatorList.filter(item=>!params.operator.value.includes(item.value))?.map(x=>x.value)) + }:{}), + ...(params.operateType?.value?.length > 0 ? { + operateType:JSON.stringify(params.operateType.include ? params.operateType.value :operatorTypeList.filter(item=>!params.operatorTypeList.value.includes(item.value))?.map(x=>x.value)) + }:{}), + } + fetchData>('audit/logs/export',{method:'GET',eoParams,eoTransformKeys:['startTime','endTime','operateType','operate_type','operate_time']}).then(response=>{ + + if (!response.ok) { + throw new Error(`Network response was not ok: ${response.status}`); + } + // 从 response 中获取文件名 + const contentDisposition = response.headers.get('Content-Disposition'); + const filenameMatch = contentDisposition && contentDisposition.match(/filename="(.+?)"/); + const filename = filenameMatch ? filenameMatch[1] : '审计日志'; + const downloadLink = document.createElement('a'); + downloadLink.href = window.URL.createObjectURL(new Blob([response.blob()])); + downloadLink.setAttribute('download', filename); + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); + }) + } + return ( + , 'multipleSelect'> + className={styles['audit-log-table']} + columns={columns} + request={(params, sort,filter) => getAuditLogList(params,sort,filter)} + rowKey="id" + options={{ + reload: false, + density: false, + setting: false, + }} + tableAlertRender={false} + size="middle" + scroll={{ y: tableHeight }} + search={{ + defaultCollapsed: false, + optionRender: (searchConfig, formProps, dom) => [ + ...dom.reverse(), + , + ], + }} + />) +} \ No newline at end of file diff --git a/frontend/packages/core/src/pages/email/Email.tsx b/frontend/packages/core/src/pages/email/Email.tsx new file mode 100644 index 00000000..e0f017ba --- /dev/null +++ b/frontend/packages/core/src/pages/email/Email.tsx @@ -0,0 +1,136 @@ +import {Alert, App, Button, Form, Input, InputNumber, Select} from "antd"; +import {useEffect, useState} from "react"; +import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; + +type EmailFieldType = { + uuid?: string; + smtpUrl?: string; + smtpPort?:number; + protocol?: string; + email?:string; + account?:string; + password?:string +}; + +const PROTOCOL_OPTIONS = [ + { label: '不设置任何协议', value: 'none' }, + { label: 'SSL协议', value: 'ssl' }, + { label: 'TLS协议', value: 'tls' } +] + +export default function Email(){ + const { message } = App.useApp() + const [form] = Form.useForm(); + const { setBreadcrumb } = useBreadcrumb() + const {fetchData} = useFetch() + const [uuid, setUuid] = useState('') + const [type,setType] = useState<'add'|'edit'>('add') + const save = () => { + form.validateFields().then(values => { + fetchData>('email',{method:type === 'add'? 'POST' : 'PUT',eoBody:({...values,...(type === 'add'? {}:{uuid})}), eoTransformKeys:['emailInfo','smtpUrl']}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + type === 'add' && setType('edit') + message.success(msg || '操作成功!') + }else{ + message.error(msg || '操作失败') + } + }) + }) + }; + + const getEmailConfig = ()=>{ + fetchData>('email',{method:'GET',eoTransformKeys:['email_info','smtp_url']}).then(response=>{ + const {code,data,msg} = response + //console.log(data) + if(code === STATUS_CODE.SUCCESS && data.emailInfo){ + form.setFieldsValue({...data.emailInfo,protocol:data.emailInfo.protocol === '' ? 'none' : data.emailInfo.protocol}) + setType('edit') + setUuid(data.emailInfo.uuid) + }else{ + message.error(msg || '操作失败') + } + }) + } + + useEffect(() => { + setBreadcrumb([ + { + title:'邮箱设置' + }, + ]) + getEmailConfig() + }, []); + + return (<> + + +
+ + label="SMTP 地址" + name="smtpUrl" + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + + + + label="SMTP 端口" + name="smtpPort" + rules={[{ required: true, message: '必填项' }]} + > + + + + + label="通信协议" + name="protocol" + rules={[{ required: true, message: '必填项' }]} + > + + + + + label="账号" + name="account" + > + + + + + label="密码" + name="password" + > + + + + + + + +
+ ) +} \ No newline at end of file diff --git a/frontend/packages/core/src/pages/logRetrieval/LogRetrieval.module.css b/frontend/packages/core/src/pages/logRetrieval/LogRetrieval.module.css new file mode 100644 index 00000000..10a3b2ee --- /dev/null +++ b/frontend/packages/core/src/pages/logRetrieval/LogRetrieval.module.css @@ -0,0 +1,12 @@ +.collapse-without-padding{ + :global .ant-collapse-header{ + background:#f7f8fa; + border-radius: 4px; + } + :global .ant-collapse-content-box{ + padding:0px !important; + } + :global .ant-list-footer{ + padding:4px 0; + } +} \ No newline at end of file diff --git a/frontend/packages/core/src/pages/logRetrieval/LogRetrieval.tsx b/frontend/packages/core/src/pages/logRetrieval/LogRetrieval.tsx new file mode 100644 index 00000000..38f4c901 --- /dev/null +++ b/frontend/packages/core/src/pages/logRetrieval/LogRetrieval.tsx @@ -0,0 +1,303 @@ + +import {App, Button, Cascader, Checkbox, Collapse, Empty, Modal, Select, Spin, Table} from "antd"; +import {useEffect, useState} from "react"; +import {ColumnsType} from "antd/es/table"; +import moment from 'moment' +import styles from './LogRetrieval.module.css' +import { saveAs } from 'file-saver' +import useWebSocket from "@common/hooks/webSocket.ts"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import {EntityItem} from "@common/const/type.ts"; +import {DefaultOptionType} from "antd/es/cascader"; +import { SimplePartition } from "../../const/partitions/types.ts"; +import MonacoEditorWrapper ,{MonacoEditorRefType} from "@common/components/aoplatform/MonacoEditorWrapper.tsx" + + type FileItemType = { + file:string, + key:string, + mod:string, + size:string +} + type OutputItemType = { + files:FileItemType[], + name:string, + tail:string +} + +type OutputItemExtraInfo = { + cluster:string + node:string +} + + +export default function LogRetrieval(){ + + const {/* modal,*/message } = App.useApp() + // const [confirmLoading, setConfirmLoading] = useState(false); + const {fetchData} = useFetch() + const { setBreadcrumb } = useBreadcrumb() + const [clusterList,setClusterList] = useState([]) + const [nodeList,setNodeList] = useState([]) + const [searchCluster, setSearchCluster] = useState([]) + const [searchNode, setSearchNode] = useState([]) + const [outputListLoading, setOutputListLoading] = useState(true) + + const [outputList, setOutputList] = useState([]) + + const handleSearch = ()=>{} + const [isModalOpen, setIsModalOpen] = useState(false); + const [currentLogFile,setCurrentLogFile] = useState() + // 打开弹窗并连接WebSocket + const handleOpenModal = (x: OutputItemType) => { + setCurrentLogFile(x) + setIsModalOpen(true); + }; + + const getOutputList = (partition:string , cluster:string, node:string)=>{ + setOutputListLoading(true) + fetchData>('log/files',{method:'GET', eoParams:{cluster, node, partition}}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setOutputList(data.output?.map((x:OutputItemType)=>{return{...x,partition:partition, cluster:cluster,node:node, + files:handlerFileData(x.files) + }})) + }else{ + message.error(msg || '操作失败') + } + setOutputListLoading(false) + }) + } + + const handlerFileData = (files:Array)=>{ + return files.sort((a:FileItemType, b:FileItemType) => ((b.file + '').localeCompare(a.file + '')))?.map((x:FileItemType) => { + x.mod = moment(x.mod).format('yyyy-MM-DD HH:mm:ss') + return x + }) + } + + const getClusterList = ()=>{ + setClusterList([]) + fetchData>('simple/partitions/cluster',{method:'GET'}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setClusterList(data.partitions?.filter((x:SimplePartition)=>x.clusters?.length > 0).map( + (x:SimplePartition)=>{ + return { + label:x.name, + value:x.id, + children:x.clusters?.map((c)=>{return{ + label:c.name, + value:c.id, + isLeaf:true} + }) || [] + }})) + setSearchCluster([data.partitions[0]?.id, data.partitions[0]?.clusters[0]?.id]) + if(data.partitions.length == 0 || data.partitions[0]?.clusters.length == 0 ) return + getNodeList(data.partitions[0]?.id,data.partitions[0]?.clusters[0]?.id) + }else{ + message.error(msg || '操作失败') + } + }) + } + + const getNodeList = (partition:string, cluster:string)=>{ + setNodeList([]) + fetchData>(`simple/partition/cluster/nodes`,{method:'GET',eoParams:{cluster}}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setNodeList(data.nodes?.map((node:EntityItem) => { + return ({ label: node.name, value: node.id }) + })) + setSearchNode(data.nodes[0].id) + getOutputList(partition,cluster,data.nodes[0].id) + }else{ + message.error(msg || '操作失败') + } + }) + } + + useEffect(()=>{ + getNodeList(searchCluster[0],searchCluster[1]) + },[searchCluster]) + + useEffect(() => { + setBreadcrumb([{ title: '日志检索'}]) + getClusterList() + }, []); + + return (<> +
+
+ + setSearchCluster(val as string[])} placeholder="请选择集群" /> +
+
+ +
+} + + +const LogTailComponent = (props:{file:OutputItemType, isVisible:boolean,onClose:()=>void})=>{ + const { + file, + isVisible,onClose} = props + const [logContent, setLogContent] = useState() + const [connected,setConnected] = useState(false) + const [trackLogs, setTrackLogs] = useState(false); + const { createWs } = useWebSocket() + const [wsRef, setWsRef]=useState() + const [editorRef, setEditorRef] = useState() + const closeConnect = () => { + wsRef?.close() + setConnected(false) + } + + const clear = () => { + setLogContent('') + } + + const connectWs = (reConnect?:boolean) => { + setWsRef(createWsRef(!reConnect)) + setConnected(true) + } + + const download = () =>{ + const vDate = new Date() + const fileName: string = `${file.name}_${vDate.getFullYear() + '-' + (vDate.getMonth() + 1) + '-' + vDate.getDate()}` + saveAs(new Blob([logContent as string || ''], { type: 'text/plain;charset=utf-8' }), `${fileName}.txt`) + } + + const updateContent = (newContent:string)=>{ + setLogContent((prevContent)=>prevContent + newContent) + trackLogs && editorRef?.revealLine(editorRef?.getModel()?.getLineCount() || 0 as number); + } + + // init=true时,为初始化ws,需要清空content;init=false时,为重连,ws连接后显示‘已恢复连接’ + const createWsRef = (init:boolean) =>{ + return createWs(`ws://${window.location.host}/api/v1/log/tail/${file.tail}`,{ + onOpen: () => init ? setLogContent('') : updateContent('\n[...已恢复连接...]\n\n'), + onClose: () => updateContent('\n[...已中断连接...]\n'), + onMessage: ( event:MessageEvent) => updateContent(event.data + '\n'), + onError: (error:Event) => console.error('ws连接出现错误:', error)}) + } + + useEffect(() => { + if(isVisible){ + const newWs = createWsRef(true) + setWsRef(newWs) + }else{ + wsRef?.close() + } + return (wsRef?.close()) + }, [isVisible]); + + return ( + +
+ setTrackLogs(e.target.checked)}> + 追踪最新日志 + + + {connected ? : + } +
+
+ + +
+ + ]} + > + { + setEditorRef(editor) + }} + /> +
) +} \ No newline at end of file diff --git a/frontend/packages/core/src/pages/logsettings/LogSettings.tsx b/frontend/packages/core/src/pages/logsettings/LogSettings.tsx new file mode 100644 index 00000000..e6af3892 --- /dev/null +++ b/frontend/packages/core/src/pages/logsettings/LogSettings.tsx @@ -0,0 +1,85 @@ + +import { Menu, MenuProps, Skeleton, message } from "antd"; +import { Link, Outlet, useNavigate, useParams } from "react-router-dom"; +import InsidePage from "@common/components/aoplatform/InsidePage"; +import { useEffect, useState } from "react"; +import { BasicResponse, STATUS_CODE } from "@common/const/const"; +import { DynamicMenuItem, } from "@common/const/type"; +import { useFetch } from "@common/hooks/http"; +import { getItem } from "@common/utils/navigation"; +import { RouterParams } from "@core/components/aoplatform/RenderRoutes"; + +const LogSettings = ()=>{ + const {moduleId} = useParams(); + const [menuItems, setMenuItems ] = useState([]) + const [activeMenu, setActiveMenu] = useState() + const {fetchData} = useFetch() + const [loading, setLoading] = useState(true) + const navigateTo = useNavigate() + + const getDynamicMenuList = ()=>{ + return fetchData>(`simple/dynamics/log`,{method:'GET'}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + const newMenu:MenuProps['items'] = data.dynamics.map((x:DynamicMenuItem)=> + getItem( + {x.title}, + x.name, + undefined, + undefined, + undefined, + 'system.devops.log_configuration.view')) + + setMenuItems(newMenu) + if(!activeMenu || activeMenu.length === 0){ + navigateTo(`/logsettings/template/${data.dynamics[0].name}`) + } + return Promise.resolve(newMenu) + }else{ + message.error(msg || '操作失败') + return Promise.reject(msg || '操作失败') + } + }) + } + + const onMenuClick: MenuProps['onClick'] = ({key}) => { + setActiveMenu(key) + }; + + + useEffect(() => { + setActiveMenu(moduleId) + }, [ moduleId]); + + useEffect(()=>{ + setLoading(true) + Promise.all([getDynamicMenuList()]).finally(()=>setLoading(false)) + },[]) + + return ( + <> + + +
+ +
+ +
+
+
+
+ + ) +} + +export default LogSettings; \ No newline at end of file diff --git a/frontend/packages/core/src/pages/logsettings/LogSettingsInstruction.tsx b/frontend/packages/core/src/pages/logsettings/LogSettingsInstruction.tsx new file mode 100644 index 00000000..0842ec7d --- /dev/null +++ b/frontend/packages/core/src/pages/logsettings/LogSettingsInstruction.tsx @@ -0,0 +1,31 @@ + +import { useBreadcrumb } from "@common/contexts/BreadcrumbContext"; +import { useEffect } from "react"; +import { Link } from "react-router-dom"; + +export default function LogSettingsInstruction() { + const { setBreadcrumb } = useBreadcrumb() + + useEffect(()=>{ + setBreadcrumb([ + {title:'日志配置'} + ]) + },[]) + return ( +
+
+

集群配置并开启日志插件

+

日志插件用于记录和管理网关的运行日志。在启用日志插件之前,请确保已经配置了集群。配置完成后,可以利用日志插件来监控和分析各项操作日志,以提高系统的可观察性和故障排查能力。

+ {/*

更多配置及关联问题,请点击帮助中心 + {/* 查看更多 * +

*/} +
+
+

环境配置

+

新增集群的地址、名称、描述和其他相关属性,以确保插件能够正确识别和连接到集群

+

添加集群地址

+
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/packages/core/src/pages/member/MemberDropdownModal.tsx b/frontend/packages/core/src/pages/member/MemberDropdownModal.tsx new file mode 100644 index 00000000..2c72941a --- /dev/null +++ b/frontend/packages/core/src/pages/member/MemberDropdownModal.tsx @@ -0,0 +1,188 @@ +import {App, Form, Input, TreeSelect} from "antd"; +import {forwardRef, useEffect, useImperativeHandle, useState} from "react"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import { MemberDropdownModalHandle, MemberDropdownModalProps, DepartmentListItem, MemberDropdownModalFieldType, MemberTableListItem } from "../../const/member/type.ts"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; + +export const MemberDropdownModal = forwardRef((props,ref)=>{ + const { message} = App.useApp() + const [form] = Form.useForm(); + const {type,entity,selectedMemberGroupId} = props + const {fetchData} = useFetch() + const [departmentList, setDepartmentList] = useState([]) + + const save:()=>Promise = ()=>{ + let url:string + let method:string + switch (type){ + case 'addDep': + case 'addChild': + url = 'user/department' + method = 'POST' + break; + case 'rename': + url = 'user/department' + method = 'PUT' + break + case 'addMember': + url = 'user/account' + method = 'POST' + break + case 'editMember': + url = 'user/account' + method = 'PUT' + break + } + return new Promise((resolve, reject)=>{ + if(!url || !method){ + reject('类型错误') + return + } + form.validateFields().then((value)=>{ + fetchData>(url, + {method, + eoBody:({ + ...value, + ...(value?.departmentIds ?{ departmentIds:Array.isArray(value?.departmentIds)? value?.departmentIds : [value?.departmentIds]}:{}), + ...(type !== 'addDep' && type !== 'addMember' && {eoParams: {id:entity!.id}}) + }),eoTransformKeys:['departmentIds']}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + useImperativeHandle(ref, ()=>({ + save + }) + ) + + const getDepartmentList = ()=>{ + fetchData>('user/departments',{method:'GET'}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setDepartmentList([{...data.departments,disabled:true}]) + }else{ + message.error(msg || '操作失败') + return {data:[], success:false} + } + }) + } + + useEffect(() => { + switch(type){ + case 'addChild': + form.setFieldsValue({parent:entity!.id}) + break + case 'rename': + form.setFieldsValue({id:entity!.id,name:entity!.name}) + break + case 'addMember': + form.setFieldsValue('-1' === selectedMemberGroupId ? {} : {departmentIds:selectedMemberGroupId}) + break + case 'editMember': + form.setFieldsValue({...entity,departmentIds:(entity as MemberTableListItem )?.department?.map(x=>x.id)}) + break + } + getDepartmentList() + + }, []); + + + return ( +
+ + {type === 'rename' && + + label="ID" + name="id" + hidden + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + + } + {(type === 'addDep' || type === 'rename') && + + label="部门名称" + name="name" + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + } + + {type === 'addChild' &&<> + + label="父部门 ID" + name="parent" + hidden + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + + + + label="子部门名称" + name="name" + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + + + } + + {(type === 'addMember'|| type ==='editMember') && <> + + label="用户名" + name="name" + rules={[{required: true, message: '必填项',whitespace:true }]} + > + + + + label="邮箱" + name="email" + rules={[{required: true, message: '必填项',whitespace:true },{type:"email",message: '不是有效邮箱地址'}]} + > + + + + label="部门" + name="departmentIds" + > + + + + } + +
) +}) \ No newline at end of file diff --git a/frontend/packages/core/src/pages/member/MemberList.tsx b/frontend/packages/core/src/pages/member/MemberList.tsx new file mode 100644 index 00000000..93adb4aa --- /dev/null +++ b/frontend/packages/core/src/pages/member/MemberList.tsx @@ -0,0 +1,419 @@ +import PageList from "@common/components/aoplatform/PageList.tsx"; +import {forwardRef, useEffect, useImperativeHandle, useRef, useState} from "react"; +import { useOutletContext, useParams} from "react-router-dom"; +import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx"; +import {ActionType, ProColumns } from "@ant-design/pro-components"; +import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import {Alert, App, Button, Select, Tree, TreeProps} from "antd"; +import {DataNode} from "antd/es/tree"; +import {FolderOpenOutlined, FolderOutlined} from "@ant-design/icons"; +import {MemberDropdownModal} from "./MemberDropdownModal.tsx"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import { AddToDepartmentHandle, AddToDepartmentProps, DepartmentListItem, MemberDropdownModalHandle, MemberTableListItem } from "../../const/member/type.ts"; +import { MEMBER_TABLE_COLUMNS } from "../../const/member/const.tsx"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import {v4 as uuidv4} from 'uuid' +import { ColumnFilterItem } from "antd/es/table/interface"; +import { handleDepartmentListToFilter } from "@common/utils/dataTransfer.ts"; +import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; +import { checkAccess } from "@common/utils/permission.ts"; +import { PERMISSION_DEFINITION } from "@common/const/permissions.ts"; +import { EntityItem } from "@common/const/type.ts"; + +const AddToDepartment = forwardRef((props,ref)=>{ + const {selectedUserIds} = props + const [selectedKeys, setSelectedKeys] = useState([]) + const [treeData,setTreeData] = useState() + const { message } = App.useApp() + const [expandedKeys, setExpandedKeys] = useState([]) + const {fetchData} = useFetch() + const save:()=>Promise = ()=>{ + return new Promise((resolve, reject)=>{ + fetchData>('user/department/member',{method:'POST',eoBody:({userIds:selectedUserIds,departmentIds:selectedKeys}),eoTransformKeys:['departmentIds','userIds']}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + useImperativeHandle(ref, ()=>({ + save + }) + ) + + const getDepartmentList = ()=>{ + fetchData>('user/departments',{method:'GET'}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + data.departments.checkable = false + data.departments.id = uuidv4() + const newId = uuidv4() + setTreeData([{ + ...data.departments, + checkable:false, + id:newId, + children:data.departments.children.filter((x)=>x.id !== 'unknown' && x.id !== 'disable')}]) + setExpandedKeys([newId]) + }else{ + message.error(msg || '操作失败') + return {data:[], success:false} + } + }) + } + + + const onCheck: TreeProps['onCheck'] = (checkedKeys:string[]) => { + setSelectedKeys(checkedKeys.checked) + }; + + useEffect(()=>{ + getDepartmentList() + + },[]) + + return ( +
+ +

请选择成员需要新加入的部门*

+
+ (e.expanded? : )} + showIcon={true} + checkStrictly={true} + selectable={false} + onCheck={onCheck} + onExpand={(expandedKeys:string[])=>{setExpandedKeys(expandedKeys)}} + treeData={treeData} + selectedKeys={[selectedKeys]} + expandedKeys={expandedKeys} + fieldNames={{title:'name',key:'id',children:'children'}} + /> +
+
) +}) + +const MemberList = ()=>{ + const { memberGroupId } = useParams(); + const [searchWord, setSearchWord] = useState('') + const { modal,message } = App.useApp() + // const [confirmLoading, setConfirmLoading] = useState(false); + const [init, setInit] = useState(true) + const {fetchData} = useFetch() + const [tableHttpReload, setTableHttpReload] = useState(true); + const [tableListDataSource, setTableListDataSource] = useState([]); + const pageListRef = useRef(null); + const {topGroupId,selectedDepartmentIds, refreshGroup} = useOutletContext<{topGroupId:string, departmentList:DepartmentListItem[],selectedDepartmentIds:string[],refreshGroup:()=>void}>() + const AddMemberRef = useRef(null) + const EditMemberRef = useRef(null) + const AddToDepRef = useRef(null) + const { setBreadcrumb } = useBreadcrumb() + const [selectedRowKeys, setSelectedRowKeys] = useState([]); + const [departmentValueEnum,setDepartmentValueEnum] = useState([]) + const {accessData} = useGlobalContext() + const [columns,setColumns] = useState[]>([]) + + const operation:ProColumns[] =[ + { + title: '操作', + key: 'option', + width: 62, + fixed:'right', + valueType: 'option', + render: (_: React.ReactNode, entity: MemberTableListItem) => [ + {openModal('editMember',entity)}} btnTitle="编辑"/>, + ], + } + ] + + + const getMemberList = ()=>{ + if(!tableHttpReload){ + setTableHttpReload(true) + return Promise.resolve({ + data: tableListDataSource, + success: true, + }); + } + return fetchData>('user/accounts',{method:'GET',eoParams:{keyword:searchWord,department:topGroupId === memberGroupId? null : memberGroupId},eoTransformKeys:['user_group']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setTableListDataSource(data.members) + setInit((prev)=>prev ? false : prev) + return {data:data.members, success: true} + }else{ + message.error(msg || '操作失败') + return {data:[], success:false} + } + }).catch(() => { + return {data:[], success:false} + }) + } + + const handleSelectChange = (newSelectedRowKeys: React.Key[]) => { + setSelectedRowKeys(newSelectedRowKeys); + }; + + const handleRowClick = (entity:MemberTableListItem)=>{ + if(entity.id === 'admin') return + setSelectedRowKeys(prevData=>prevData?.indexOf(entity.id) === -1 ? [...prevData,entity.id] : prevData.filter((x)=>x !== entity.id)) + } + + const manualReloadTable = () => { + setTableHttpReload(true); // 表格数据需要从后端接口获取 + pageListRef.current?.reload() + }; + + const handleMemberAction = (type:'removeFromDep'|'blocked'|'activate'|'delete')=>{ + let url:string + let method:string + let params:{[k:string]:unknown} = {} + let body:{[k:string]:unknown} = {} + switch(type){ + case 'removeFromDep': + url ='user/department/member/remove' + method = 'POST' + params = {department:memberGroupId} + body = {userIds:selectedRowKeys} + break; + case 'blocked': + url = 'user/account/disable' + method = 'POST' + body = {userIds:selectedRowKeys} + break; + case 'activate': + url = 'user/account/enable' + method = 'POST' + body = {userIds:selectedRowKeys} + break; + case 'delete': + url = 'user/account' + method = 'DELETE' + params = {ids:JSON.stringify(selectedRowKeys)} + body = {userIds:selectedRowKeys} + break; + } + + return new Promise((resolve, reject)=>{ + fetchData>(url,{method,eoTransformKeys:['user_ids','userIds'],eoParams:params,eoBody:(body)}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + })} + + const isActionAllowed = (type:'addMember'|'editMember'|'removeFromDep'|'addToDep'|'blocked'|'activate'|'delete') => { + const actionToPermissionMap = { + 'addMember': 'add', + 'editMember': 'edit', + 'removeFromDep': 'remove', + 'addToDep': 'add', + 'activate': 'block', + 'blocked': 'block', + 'delete': 'delete' + }; + + const action = actionToPermissionMap[type]; + const permission :keyof typeof PERMISSION_DEFINITION[0]= `system.organization.member.${action}`; + + return !checkAccess(permission, accessData); + }; + + const openModal = (type:'addMember'|'editMember'|'removeFromDep'|'addToDep'|'blocked'|'activate'|'delete',entity?:MemberTableListItem)=>{ + let title:string = '' + let content:string|React.ReactNode = '' + switch (type){ + case 'addMember': + title='添加账号' + content= + break; + case 'editMember': + title='编辑成员信息' + content= + break; + case 'removeFromDep': + title='移出当前部门' + content=确定将成员从当前部门中移除?此操作无法恢复,确认操作? + break; + case 'addToDep': + title='加入部门' + content= + break; + case 'delete': + title='删除' + content=确定删除成员?此操作无法恢复,确认操作? + break; + case 'blocked': + title='禁用成员' + content=确定禁用成员?此操作无法恢复,确认操作? + break; + case 'activate': + title='启用成员' + content=确定启用成员?此操作无法恢复,确认操作? + break; + } + + modal.confirm({ + title, + content, + onOk:()=>{ + switch (type){ + case 'addMember': + return AddMemberRef.current?.save().then((res)=>{if(res === true) {refreshGroup && refreshGroup();manualReloadTable()}}) + case 'editMember': + //console.log('addChild') + return EditMemberRef.current?.save().then((res)=>{if(res === true){refreshGroup && refreshGroup();manualReloadTable()}}) + case 'removeFromDep': + //console.log('addChild') + return handleMemberAction('removeFromDep').then((res)=>{if(res === true){refreshGroup && refreshGroup();manualReloadTable()}}) + case 'addToDep': + //console.log('addToDep') + return AddToDepRef.current?.save().then((res)=>{if(res === true) {refreshGroup && refreshGroup();manualReloadTable()}}) + case 'activate': + return handleMemberAction('activate').then((res)=>{if(res === true){refreshGroup && refreshGroup();manualReloadTable()}}) + case 'blocked': + return handleMemberAction('blocked').then((res)=>{if(res === true){refreshGroup && refreshGroup();manualReloadTable()}}) + case 'delete': + return handleMemberAction('delete').then((res)=>{if(res === true){refreshGroup && refreshGroup();manualReloadTable()}}) + } + }, + width:600, + okText:'确认', + okButtonProps:{ + disabled : isActionAllowed(type) + }, + cancelText:'取消', + closable:true, + icon:<>, + }) + } + + useEffect(() => { + !init && manualReloadTable() + setSelectedRowKeys([]) + }, [memberGroupId]); + + useEffect(()=>{ + getRoleList() + setBreadcrumb([{ title: '成员与部门'}]) + getDepartmentList() + },[]) + + const getDepartmentList = async ()=>{ + setDepartmentValueEnum([]) + const {code,data,msg} = await fetchData>('simple/departments',{method:'GET'}) + if(code === STATUS_CODE.SUCCESS){ + const tmpValueEnum:ColumnFilterItem[] = [{text:data.department.name, value:data.department.id,children:handleDepartmentListToFilter(data.department.children)}] + setDepartmentValueEnum(tmpValueEnum) + }else{ + message.error(msg || '操作失败') + } + } + + const changeMemberInfo = (value:string[],entity:MemberTableListItem )=>{ + //console.log(value) + return new Promise((resolve, reject) => { + fetchData>(`account/role`, {method: 'PUT',eoBody:({roles:value, users:[entity.id]})}).then(response => { + const {code, msg} = response + if (code === STATUS_CODE.SUCCESS) { + message.success(msg || '操作成功!') + resolve(true) + } else { + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + const getRoleList = ()=>{ + fetchData}>>('simple/roles', {method: 'GET', eoParams: {group:'system'}}).then(response => { + const {code, data,msg} = response + if (code === STATUS_CODE.SUCCESS) { + const newCol = [...MEMBER_TABLE_COLUMNS] + for(const col of newCol){ + if(col.dataIndex === 'roles'){ + col.render = (_,entity)=>( + + debounce(onSearchWordChange, 100)(e.target.value)} + allowClear placeholder="搜索部门" + prefix={}/> + +
+
+ } + blockNode={true} + treeData={treeData} + selectedKeys={[selectedDepartmentId]} + expandedKeys={expandedKeys} + onExpand={(expandedKeys:Key[])=>{setExpandedKeys(expandedKeys)}} + onSelect={(selectedKeys,selectedRow) => { + if(selectedKeys.length > 0 ){ + setSelectedDepartmentIds((selectedRow.node as unknown).departmentIds || []) + navigate(`/member/list${selectedKeys[0] === '-1'? '' : `/${selectedKeys[0]}`}`) + } + }} + /> + {/* } + blockNode={true} + treeData={treeData} + selectedKeys={[selectedDepartmentId]} + expandedKeys={expandedKeys} + onExpand={(expandedKeys:string[])=>{setExpandedKeys(expandedKeys)}} + onSelect={(selectedKeys,selectedRow) => { + setSelectedDepartmentIds((selectedRow.node as unknown).departmentIds || []) + navigate(`/member/list${selectedKeys[0] === '-1' ? '' : `/${selectedKeys[0]}`}`) + }} + /> */} +
+
+ +
+ getDepartmentList()}}/> +
+ + ); +} +export default MemberPage; \ No newline at end of file diff --git a/frontend/packages/core/src/pages/member/Modal/AddDepModal.tsx b/frontend/packages/core/src/pages/member/Modal/AddDepModal.tsx new file mode 100644 index 00000000..1e41f062 --- /dev/null +++ b/frontend/packages/core/src/pages/member/Modal/AddDepModal.tsx @@ -0,0 +1,79 @@ +import { App, Form, Input } from "antd"; +import { forwardRef, useImperativeHandle, useEffect } from "react"; +import WithPermission from "@common/components/aoplatform/WithPermission"; +import { BasicResponse, STATUS_CODE } from "@common/const/const"; +import { MemberDropdownModalHandle, MemberDropdownModalProps, MemberDropdownModalFieldType } from "../../../const/member/type"; +import { useFetch } from "@common/hooks/http"; + +export const AddDepModal = forwardRef((props,ref)=>{ + const { message} = App.useApp() + const [form] = Form.useForm(); + const {type,entity} = props + const {fetchData} = useFetch() + + const save:()=>Promise = ()=>{ + return new Promise((resolve, reject)=>{ + form.validateFields().then((value)=>{ + fetchData>('user/department', + {method:'POST', + eoBody:({ + ...value, + ...(value?.departmentIds ?{ departmentIds:Array.isArray(value?.departmentIds)? value?.departmentIds : [value?.departmentIds]}:{}), + ...(type !== 'addDep' && type !== 'addMember' && {eoParams: {id:entity!.id}}) + }),eoTransformKeys:['departmentIds']}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + useImperativeHandle(ref, ()=>({ + save + }) + ) + + useEffect(() => { + type === 'addChild'&& form.setFieldsValue({parent:entity!.id}) + }, []); + + + return ( +
+ + {type === 'addChild' && + label="父部门 ID" + name="parent" + hidden + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + } + + + label={`${type === 'addChild' ? '子' : ''}部门名称`} + name="name" + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + + + +
) +}) \ No newline at end of file diff --git a/frontend/packages/core/src/pages/member/Modal/AddToDepartmentModal.tsx b/frontend/packages/core/src/pages/member/Modal/AddToDepartmentModal.tsx new file mode 100644 index 00000000..a980b305 --- /dev/null +++ b/frontend/packages/core/src/pages/member/Modal/AddToDepartmentModal.tsx @@ -0,0 +1,88 @@ +import { FolderOpenOutlined, FolderOutlined } from "@ant-design/icons" +import { App, TreeProps, Alert, Tree,DataNode } from "antd" +import { forwardRef, useState, useImperativeHandle, useEffect } from "react" +import { BasicResponse, STATUS_CODE } from "@common/const/const" +import { AddToDepartmentHandle, AddToDepartmentProps, DepartmentListItem } from "../../../const/member/type" +import { useFetch } from "@common/hooks/http" +import {v4 as uuidv4} from 'uuid' + +const AddToDepartmentModal = forwardRef((props,ref)=>{ + const {selectedUserIds} = props + const [selectedKeys, setSelectedKeys] = useState([]) + const [treeData,setTreeData] = useState() + const { message } = App.useApp() + const [expandedKeys, setExpandedKeys] = useState([]) + const {fetchData} = useFetch() + const save:()=>Promise = ()=>{ + return new Promise((resolve, reject)=>{ + fetchData>('user/department/member',{method:'POST',eoBody:({userIds:selectedUserIds,departmentIds:selectedKeys}),eoTransformKeys:['departmentIds','userIds']}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + useImperativeHandle(ref, ()=>({ + save + }) + ) + + const getDepartmentList = ()=>{ + fetchData>('user/departments',{method:'GET'}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + data.departments.checkable = false + data.departments.id = uuidv4() + const newId = uuidv4() + setTreeData([{ + ...data.departments, + checkable:false, + id:newId, + children:data.departments.children.filter((x)=>x.id !== 'unknown' && x.id !== 'disable')}]) + setExpandedKeys([newId]) + }else{ + message.error(msg || '操作失败') + return {data:[], success:false} + } + }) + } + + + const onCheck: TreeProps['onCheck'] = (checkedKeys:string[]) => { + setSelectedKeys(checkedKeys.checked) + }; + + useEffect(()=>{ + getDepartmentList() + + },[]) + + return ( +
+ +

请选择成员需要新加入的部门*

+
+ (e.expanded? : )} + showIcon={true} + checkStrictly={true} + selectable={false} + onCheck={onCheck} + onExpand={(expandedKeys:string[])=>{setExpandedKeys(expandedKeys)}} + treeData={treeData} + selectedKeys={[selectedKeys]} + expandedKeys={expandedKeys} + fieldNames={{title:'name',key:'id',children:'children'}} + /> +
+
) +}) + +export default AddToDepartmentModal \ No newline at end of file diff --git a/frontend/packages/core/src/pages/member/Modal/EditMember.tsx b/frontend/packages/core/src/pages/member/Modal/EditMember.tsx new file mode 100644 index 00000000..ffa6cd8a --- /dev/null +++ b/frontend/packages/core/src/pages/member/Modal/EditMember.tsx @@ -0,0 +1,115 @@ + +import { App, Form, Input, TreeSelect } from "antd"; +import { forwardRef, useState, useImperativeHandle, useEffect } from "react"; +import WithPermission from "@common/components/aoplatform/WithPermission"; +import { BasicResponse, STATUS_CODE } from "@common/const/const"; +import { MemberDropdownModalHandle, MemberDropdownModalProps, DepartmentListItem, MemberTableListItem, MemberDropdownModalFieldType } from "../../../const/member/type"; +import { useFetch } from "@common/hooks/http"; + +export const EditMemberModal = forwardRef((props,ref)=>{ + const { message} = App.useApp() + const [form] = Form.useForm(); + const {type,entity,selectedMemberGroupId} = props + const {fetchData} = useFetch() + const [departmentList, setDepartmentList] = useState([]) + + const save:()=>Promise = ()=>{ + return new Promise((resolve, reject)=>{ + form.validateFields().then((value)=>{ + fetchData>('user/account', + {method:type === 'addMember' ? 'POST' : 'PUT', + eoBody:({ + ...value, + ...(value?.departmentIds ?{ departmentIds:Array.isArray(value?.departmentIds)? value?.departmentIds : [value?.departmentIds]}:{}), + ...(type !== 'addDep' && type !== 'addMember' && {eoParams: {id:entity!.id}}) + }),eoTransformKeys:['departmentIds']}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + useImperativeHandle(ref, ()=>({ + save + }) + ) + + const getDepartmentList = ()=>{ + fetchData>('user/departments',{method:'GET'}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setDepartmentList([{...data.departments,children:data.departments.children?.filter((x)=>['unknown','disable'].indexOf(x.id) === -1),disabled:true}]) + }else{ + message.error(msg || '操作失败') + return {data:[], success:false} + } + }) + } + + useEffect(() => { + switch(type){ + case 'addMember': + form.setFieldsValue( (!selectedMemberGroupId || ['-1','disable','unknown'].indexOf(selectedMemberGroupId.toString()) !== -1 )? {} : {departmentIds:[selectedMemberGroupId]}) + break + case 'editMember': + form.setFieldsValue({...entity,departmentIds:(entity as MemberTableListItem )?.department?.map(x=>x.id)}) + break + } + getDepartmentList() + + }, []); + + + return ( +
+ + label="用户名" + name="name" + rules={[{required: true, message: '必填项',whitespace:true }]} + > + + + + label="邮箱" + name="email" + rules={[{required: true, message: '必填项',whitespace:true },{type:"email",message: '不是有效邮箱地址'}]} + > + + + + label="部门" + name="departmentIds" + > + + + +
) +}) \ No newline at end of file diff --git a/frontend/packages/core/src/pages/member/Modal/RenameDepModal.tsx b/frontend/packages/core/src/pages/member/Modal/RenameDepModal.tsx new file mode 100644 index 00000000..91d3710a --- /dev/null +++ b/frontend/packages/core/src/pages/member/Modal/RenameDepModal.tsx @@ -0,0 +1,73 @@ + +import { App, Form, Input } from "antd"; +import { forwardRef, useImperativeHandle, useEffect } from "react"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import { BasicResponse, STATUS_CODE } from "@common/const/const.ts"; +import { MemberDropdownModalHandle, MemberDropdownModalProps, MemberDropdownModalFieldType } from "../../../const/member/type"; +import { useFetch } from "@common/hooks/http.ts"; + +export const RenameDepModal = forwardRef((props,ref)=>{ + const { message} = App.useApp() + const [form] = Form.useForm(); + const {entity} = props + const {fetchData} = useFetch() + + const save:()=>Promise = ()=>{ + return new Promise((resolve, reject)=>{ + form.validateFields().then((value)=>{ + fetchData>('user/department', + {method:'PUT', + eoBody:({ + ...value, + }),eoParams: {id:entity!.id}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + useImperativeHandle(ref, ()=>({ + save + }) + ) + + useEffect(() => { + form.setFieldsValue({id:entity!.id,name:entity!.name}) + }, []); + + + return ( +
+ + label="ID" + name="id" + hidden + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + + + label="部门名称" + name="name" + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + + +
) +}) \ No newline at end of file diff --git a/frontend/packages/core/src/pages/partitions/PartitionInsideCert.tsx b/frontend/packages/core/src/pages/partitions/PartitionInsideCert.tsx new file mode 100644 index 00000000..17693386 --- /dev/null +++ b/frontend/packages/core/src/pages/partitions/PartitionInsideCert.tsx @@ -0,0 +1,306 @@ +import PageList from "@common/components/aoplatform/PageList.tsx" +import {ActionType, ProColumns} from "@ant-design/pro-components"; +import {FC, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState} from "react"; +import {Link, useParams} from "react-router-dom"; +import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import {App, Button, Col, Divider, Form, Input, Row, Upload} from "antd"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx"; +import { Base64 } from 'js-base64'; +import { PARTITION_CERT_TABLE_COLUMNS } from "../../const/partitions/const.tsx"; +import { PartitionCertConfigHandle, PartitionCertConfigProps, PartitionCertTableListItem } from "../../const/partitions/types.ts"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import { SimpleMemberItem } from "@common/const/type.ts"; +import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; +import { checkAccess } from "@common/utils/permission.ts"; +import { PERMISSION_DEFINITION } from "@common/const/permissions.ts"; + +const CertConfigModal = forwardRef((props, ref) => { + const { message } = App.useApp() + const {type,entity} = props + const [form] = Form.useForm(); + const [, forceUpdate] = useState(null); + const {fetchData} = useFetch() + + const save:()=>Promise = ()=>{ + return new Promise((resolve, reject)=>{ + form.validateFields().then((value)=>{ + const body = { + key:Base64.encode(value.key), + pem:Base64.encode(value.pem) + } + fetchData>('certificate',{method:type === 'add'? 'POST' : 'PUT',eoBody:(body), eoParams:type === 'add' ? {}:{id:entity!.id}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + useImperativeHandle(ref, ()=>({ + save + }) + ) + + useEffect(() => { + if(type === 'edit' && entity){ + //console.log(entity) + form.setFieldsValue({key:Base64.decode(entity.key), pem:Base64.decode(entity.pem)}) + forceUpdate({}) + } + }, []); + + return ( +
+
+ + { + const reader = new FileReader(); + reader.readAsText(file); // 如果你想要纯文本 + reader.onload = ()=>{ + const result = reader.result; + form.setFieldsValue({key: result}); // 更新表单的密钥字段 + forceUpdate({}) + } + return false; // 阻止自动上传 + }}> + + + + + +
+ { + form.setFieldsValue({key: e.target.value}); // 当用户手动输入时更新表单的密钥字段 + forceUpdate({}) + }} + /> + + + + +
+ + { + const reader = new FileReader(); + reader.readAsText(file); + reader.onload = ()=>{ + const {result} = reader + form.setFieldsValue({pem: result}) + forceUpdate({}) + } + return false + }}> + + + + + + +
+ { + form.setFieldsValue({pem: e.target.value}); // 当用户手动输入时更新表单的密钥字段 + forceUpdate({}) + }}/> + + + + + ) +}) + +const PartitionInsideCert:FC = ()=>{ + const { setBreadcrumb } = useBreadcrumb() + const { modal,message } = App.useApp() + const [init, setInit] = useState(true) + const {fetchData} = useFetch() + const addRef = useRef(null) + const editRef = useRef(null) + const pageListRef = useRef(null); + const [memberValueEnum, setMemberValueEnum] = useState<{[k:string]:{text:string}}>({}) + const {accessData} = useGlobalContext() + + const getPartitionCertList =(): Promise<{ data: PartitionCertTableListItem[], success: boolean }>=> { + return fetchData>('certificates',{method:'GET',eoTransformKeys:['partition_id','update_time','not_before','not_after']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setInit((prev)=>prev ? false : prev) + return {data:data.certificates, success: true} + }else{ + message.error(msg || '操作失败') + return {data:[], success:false} + } + }).catch(() => { + return {data:[], success:false} + }) + } + + const deleteCert = (entity:PartitionCertTableListItem)=>{ + return new Promise((resolve, reject)=>{ + fetchData>('certificate',{method:'DELETE',eoParams:{id:entity.id}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + const openModal = async (type:'add'|'edit'|'delete', entity?:PartitionCertTableListItem)=>{ + let title:string = '' + let content:string | React.ReactNode= '' + switch (type){ + case 'add': + title='添加证书' + content= + break; + case 'edit':{ + title='修改证书' + message.loading('正在加载数据') + const {code,data,msg} = await fetchData>('certificate',{method:'GET',eoParams:{id:entity!.id}}) + message.destroy() + if(code === STATUS_CODE.SUCCESS){ + content= + }else{ + message.error(msg || '操作失败') + return + } + break;} + case 'delete': + title='删除' + content='该数据删除后将无法找回,请确认是否删除?' + break; + } + + modal.confirm({ + title, + content, + onOk:()=> { + switch (type){ + case 'add': + return addRef.current?.save().then((res)=>{if(res === true) manualReloadTable()}) + case 'edit': + return editRef.current?.save().then((res)=>{if(res === true) manualReloadTable()}) + case 'delete': + return deleteCert(entity!).then((res)=>{if(res === true) manualReloadTable()}) + } + }, + width:600, + okText:'确认', + okButtonProps:{ + disabled : !checkAccess( `system.devops.ssl_certificate.${type}` as keyof typeof PERMISSION_DEFINITION[0], accessData) + }, + cancelText:'取消', + closable:true, + icon:<>, + }) + } + + + const manualReloadTable = () => { + pageListRef.current?.reload() + }; + + const operation:ProColumns[] =[ + { + title: '操作', + key: 'option', + width: 105, + fixed:'right', + valueType: 'option', + render: (_: React.ReactNode, entity: PartitionCertTableListItem) => [ + {openModal('edit',entity)}} btnTitle="编辑"/>, + , + {openModal('delete',entity)}} btnTitle="删除"/>] + } + ] + + useEffect(() => { + setBreadcrumb([ + {title:'证书管理'} + ]) + getMemberList() + manualReloadTable() + }, []); + + const getMemberList = async ()=>{ + setMemberValueEnum({}) + const {code,data,msg} = await fetchData>('simple/member',{method:'GET'}) + if(code === STATUS_CODE.SUCCESS){ + const tmpValueEnum:{[k:string]:{text:string}} = {} + data.members?.forEach((x:SimpleMemberItem)=>{ + tmpValueEnum[x.name] = {text:x.name} + }) + setMemberValueEnum(tmpValueEnum) + }else{ + message.error(msg || '操作失败') + } + } + + const columns = useMemo(()=>{ + return PARTITION_CERT_TABLE_COLUMNS.map(x=>{if(x.filters &&((x.dataIndex as string[])?.indexOf('updater') !== -1) ){x.valueEnum = memberValueEnum} return x}) + },[memberValueEnum]) + + return ( +
+
+

证书

+

通过为 API 服务配置和管理 SSL 证书,企业可以加密数据传输,防止敏感信息被窃取或篡改。

+
+ getPartitionCertList()} + showPagination={false} + addNewBtnTitle="添加证书" + addNewBtnAccess="system.devops.ssl_certificate.add" + onAddNewBtnClick={()=>{openModal('add')}} + onRowClick={(row:PartitionCertTableListItem)=>openModal('edit',row)} + tableClickAccess="system.devops.ssl_certificate.edit" + /> +
+ ) + +} +export default PartitionInsideCert \ No newline at end of file diff --git a/frontend/packages/core/src/pages/partitions/PartitionInsideCluster.tsx b/frontend/packages/core/src/pages/partitions/PartitionInsideCluster.tsx new file mode 100644 index 00000000..a41aa03d --- /dev/null +++ b/frontend/packages/core/src/pages/partitions/PartitionInsideCluster.tsx @@ -0,0 +1,117 @@ +import { FC, useEffect, useRef, useState} from "react"; +import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import {App, Button, Col, Collapse, Empty, Row, Spin, Tag} from "antd"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import { NodeModalHandle, PartitionClusterNodeTableListItem } from "../../const/partitions/types.ts"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; +import { ClusterNodeModal } from "./PartitionInsideClusterNode.tsx"; +import { DownOutlined, LoadingOutlined, UpOutlined } from "@ant-design/icons"; +import { checkAccess } from "@common/utils/permission.ts"; + +const PartitionInsideCluster:FC = ()=> { + const {setBreadcrumb} = useBreadcrumb() + const {modal, message} = App.useApp() + const {fetchData} = useFetch() + const [nodesList, setNodesList] = useState() + const [loading, setLoading] = useState(false) + const {accessData} = useGlobalContext() + const [activeKey, setActiveKey] = useState([]) + const editNodeRef = useRef(null) + + const getPartitionClusterInfo = () => { + setNodesList([]) + setLoading(true) + return fetchData>('cluster/nodes', {method: 'GET',eoTransformKeys:['manager_address','service_address','peer_address']}).then(response => { + const {code, data, msg} = response + if (code === STATUS_CODE.SUCCESS) { + setNodesList(data.nodes) + setActiveKey(data.nodes.map((x:PartitionClusterNodeTableListItem)=>x.id)) + } else { + message.error(msg || '操作失败') + } + }).catch(() => { + return {data: [], success: false} + }).finally(()=>{ + setLoading(false) + }) + } + + const openModal = async (type:'editNode')=>{ + let title:string = '' + let content:string|React.ReactNode = '' + + switch(type){ + case 'editNode': { + title = '重置配置' + content = + } + break; + } + + modal.confirm({ + title, + content, + onOk:()=> { + switch (type){ + case 'editNode': + return editNodeRef.current?.save().then((res:boolean)=>{if(res === true) getPartitionClusterInfo(); return false}) + } + }, + width:type === 'editNode' ? 900 : 600, + okText:'确认', + okButtonProps:{ + disabled:!checkAccess('system.devops.cluster.edit', accessData) + }, + cancelText:'取消', + closable:true, + icon:<>, + }) + } + + useEffect(() => { + setBreadcrumb([ + {title: '集群'} + ]) + getPartitionClusterInfo() + }, []); + + return ( + <> +
+ +
+

集群

+

设置访问 API 的集群,让 API 在分布式环境中稳定运行,并且能够根据业务需求进行灵活扩展和优化。

+
+ +
+ } spinning={loading}> +
+ {nodesList && nodesList.length > 0 ? + (isActive? : )} + items={nodesList?.map(x=>{ + return { + label:
{x.status === 1 ? '正常' : '异常'}{x.managerAddress.join(',')}
, + key:x.id, + children:
+
管理地址:{x.managerAddress.map(m=>(

{m}

))} + 服务地址:{x.serviceAddress.map(m=>(

{m}

))} + 同步地址:

{x.peerAddress}

+ + } + })} + activeKey={activeKey} + onChange={(val)=>{setActiveKey(val as string[])}} + />: + } + + + + + ) +} + +export default PartitionInsideCluster \ No newline at end of file diff --git a/frontend/packages/core/src/pages/partitions/PartitionInsideClusterNode.tsx b/frontend/packages/core/src/pages/partitions/PartitionInsideClusterNode.tsx new file mode 100644 index 00000000..81eb676d --- /dev/null +++ b/frontend/packages/core/src/pages/partitions/PartitionInsideClusterNode.tsx @@ -0,0 +1,93 @@ + +import {forwardRef, useImperativeHandle, useState} from "react"; +import {App, Button, Form, Input, Table} from "antd"; +import {useFetch} from "@common/hooks/http.ts"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import { NODE_MODAL_COLUMNS } from "../../const/partitions/const.tsx"; +import { NodeModalHandle, PartitionClusterNodeModalTableListItem, PartitionClusterNodeTableListItem, NodeModalFieldType } from "../../const/partitions/types.ts"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; + +export const ClusterNodeModal = forwardRef((_,ref)=>{ + const { message } = App.useApp() + const [form] = Form.useForm(); + const [dataSource,setDataSource] = useState([]) + const {fetchData} = useFetch() + + const test = ()=>{ + setDataSource([]) + return new Promise((resolve, reject)=>{ + form.validateFields().then((value)=> { + fetchData>('cluster/check', {method: 'POST', eoBody: (value),eoTransformKeys:['manager_address','service_address','peer_address']}).then(response => { + const {code,data, msg} = response + if (code === STATUS_CODE.SUCCESS) { + message.success(msg || '操作成功') + setDataSource(data.nodes) + } else { + message.error(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }).catch((errorInfo)=> reject(errorInfo)) + })} + + const save:()=>Promise = ()=>{ + return new Promise((resolve, reject)=>{ + form.validateFields().then(()=> { + fetchData>('cluster/reset',{method:'PUT' ,eoBody:({managerAddress:form.getFieldValue('address')}), eoTransformKeys:['managerAddress']}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + useImperativeHandle(ref, ()=>({ + save + }) + ) + + return ( + +
+
+ + label="集群地址" + name="address" + className="p-0 bg-transparent rounded-none border-none flex-1" + rules={[{ required: true, message: '必填项' }]} + > + test()}/> + +
+ +
+
+ { + dataSource.length > 0 && +
+ } + + + ) +}) diff --git a/frontend/packages/core/src/pages/resourcesettings/ResourceSettings.tsx b/frontend/packages/core/src/pages/resourcesettings/ResourceSettings.tsx new file mode 100644 index 00000000..209832f4 --- /dev/null +++ b/frontend/packages/core/src/pages/resourcesettings/ResourceSettings.tsx @@ -0,0 +1,83 @@ + +import { Menu, MenuProps, Skeleton, message } from "antd"; +import { Link, Outlet, useNavigate, useParams } from "react-router-dom"; +import InsidePage from "@common/components/aoplatform/InsidePage"; +import { useEffect, useState } from "react"; +import { BasicResponse, STATUS_CODE } from "@common/const/const"; +import { DynamicMenuItem } from "@common/const/type"; +import { useFetch } from "@common/hooks/http"; +import { getItem } from "@common/utils/navigation"; +import { RouterParams } from "@core/components/aoplatform/RenderRoutes"; + +const LogSettings = ()=>{ + const {moduleId} = useParams(); + const [menuItems, setMenuItems ] = useState([]) + const [activeMenu, setActiveMenu] = useState() + const {fetchData} = useFetch() + const [loading, setLoading] = useState(true) + const navigateTo = useNavigate() + + const getDynamicMenuList = ()=>{ + setLoading(true) + fetchData>(`simple/dynamics/resource`,{method:'GET'}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + const newMenu:MenuProps['items'] = data.dynamics.map((x:DynamicMenuItem)=> + getItem( + {x.title}, + x.name, + undefined, + undefined, + undefined, + 'system.partition.self.view')) + + setMenuItems(newMenu) + if(!activeMenu || activeMenu.length === 0){ + navigateTo(`/resourcesettings/template/${data.dynamics[0].name}`) + } + }else{ + message.error(msg || '操作失败') + } + }).finally(()=>setLoading(false)) + } + + const onMenuClick: MenuProps['onClick'] = ({key}) => { + setActiveMenu(key) + }; + + useEffect(() => { + setActiveMenu(moduleId) + }, [ moduleId]); + + useEffect(()=>{ + setLoading(true) + getDynamicMenuList() + },[]) + + + return ( + <> + + +
+ +
+ +
+
+
+
+ + ) +} + +export default LogSettings; \ No newline at end of file diff --git a/frontend/packages/core/src/pages/resourcesettings/ResourceSettingsInstruction.tsx b/frontend/packages/core/src/pages/resourcesettings/ResourceSettingsInstruction.tsx new file mode 100644 index 00000000..b1cd0af1 --- /dev/null +++ b/frontend/packages/core/src/pages/resourcesettings/ResourceSettingsInstruction.tsx @@ -0,0 +1,32 @@ +import { useBreadcrumb } from "@common/contexts/BreadcrumbContext"; +import { useEffect } from "react"; +import { Link } from "react-router-dom"; + +export default function ResourceSettingsInstruction() { + + const { setBreadcrumb } = useBreadcrumb() + + useEffect(()=>{ + setBreadcrumb([ + {title:'资源配置'} + ]) + },[]) + + return ( +
+
+

集群配置并开启资源插件

+

资源插件用于增强网关的功能和性能。在启用资源类插件之前,请确保已经配置了集群。例如,Redis插件可以提高缓存和速率限制的性能,配置完成后,可以使用Redis作为缓存数据库。

+ {/*

更多配置及关联问题,请点击帮助中心 + {/* 查看更多 +

*/} +
+
+

集群配置

+

新增集群地址、描述和其他相关属性,以确保插件能够正确识别和连接到集群

+

配置集群地址

+
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/packages/core/src/pages/role/RoleConfig.tsx b/frontend/packages/core/src/pages/role/RoleConfig.tsx new file mode 100644 index 00000000..21aab980 --- /dev/null +++ b/frontend/packages/core/src/pages/role/RoleConfig.tsx @@ -0,0 +1,245 @@ +import { useEffect, useMemo, useState} from "react"; +import {App, Button, Checkbox, Collapse, Form, GetProp, Input} from "antd"; +import {useFetch} from "@common/hooks/http.ts"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import { useNavigate, useParams } from "react-router-dom"; +import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx"; +import { ArrowLeftOutlined, LeftOutlined } from "@ant-design/icons"; + +type PermissionItem = { + name:string + cname:string + value:string +} + +type PermissionClassify = PermissionItem & {children : ( PermissionItem & {dependents:string[]})[]} + +type RolePermissionItem = PermissionItem & { + children:PermissionClassify[]} + + +type DependenciesMapType = Map + +type PermissionCollapseProps = { + id?: string; + value?: string[]; + onChange?: (value:string[]) => void; + permissionTemplate:RolePermissionItem[] + dependenciesMap?: DependenciesMapType +} + +type PermissionInfo = { + permit: string[] + description: string + update_time: string + create_time: string + name: string +} + +const PermissionContent = ({permits,onChange,value=[],id,dependenciesMap}:{permits:PermissionClassify[],dependenciesMap:DependenciesMapType,value:string[],id:string, onChange?: (value:string[]) => void;})=>{ + + const onSingleCheckboxChange: GetProp = (e) => { + if(e.target.checked){ + onChange?.(Array.from(new Set([...value, e.target.id, ...(dependenciesMap?.get(e.target.id!)?.dependents || [])] as string[]))) + }else{ + const cancelValue = [...dependenciesMap?.get(e.target.id!)?.control || [], e.target.id] + onChange?.(value.filter(x=>!cancelValue.includes(x))) + } + }; + + return ( +
+ { + permits.map((item:PermissionClassify)=>( + <> +
+ {item.cname !== '' &&

{item.cname}

} +
+ {item.children.map(x=> 0 && value.indexOf(x.value)>-1} onChange={onSingleCheckboxChange}>{x.cname})} +
+
+ + )) + }
+ ) +} + // 自定义表单控件 + const PermissionCollapse:React.FC = (props)=>{ + const { id, value = [], onChange,permissionTemplate ,dependenciesMap} = props; + const [openCollapses, setOpenCollapses] = useState([]) + + const items = useMemo(()=>{ + const generatePermissionItem = (permissionItem:RolePermissionItem[])=> permissionItem.map((item:RolePermissionItem)=>({ + key:item.name, + label:item.cname, + children:onChange?.(e)} id={id!} dependenciesMap={dependenciesMap!}/> + })) + return permissionTemplate && permissionTemplate.length > 0 ? generatePermissionItem(permissionTemplate) : [] + },[permissionTemplate,value]) + + useEffect(()=>{ + permissionTemplate && setOpenCollapses(permissionTemplate?.map(x=>x.name)) + },[permissionTemplate]) + + const onCollapseChange = (keys: string | string[]) => { + setOpenCollapses(keys as string[]) + }; + + return + } + + +const RoleConfig = ()=>{ + const { message } = App.useApp() + const [form] = Form.useForm(); + // const [formData, dispatch] = useReducer(formReducer, {}); + // const [dataSource,setDataSource] = useState([]) + const {fetchData} = useFetch() + const navigateTo = useNavigate() + const { roleType, roleId} = useParams() + const [permissionTemplate, setPermissionTemplate] = useState() + const [dependenciesMap, setDependenciesMap] = useState() + const APP_MODE = import.meta.env.VITE_APP_MODE; + + + const generateDependenciesMap = (data:RolePermissionItem[])=>{ + const map = new Map() + data.forEach((item:RolePermissionItem)=>{ + item.children.forEach((child:PermissionClassify)=>{ + child.children.forEach((permission:PermissionItem & {dependents:string[]})=>{ + + if (permission.dependents && permission.dependents.length > 0) { + // 获取当前权限的依赖 + const currentDependents = map.get(permission.value); + if (currentDependents) { + currentDependents.dependents.push(...permission.dependents); + } else { + map.set(permission.value, { dependents: [...permission.dependents], control: [] }); + } + + // 更新依赖项的控制项 + permission.dependents.forEach((dependent: string) => { + const dependentEntry = map.get(dependent); + if (dependentEntry) { + dependentEntry.control.push(permission.value); + } else { + map.set(dependent, { dependents: [], control: [permission.value] }); + } + }); + } + }) + }) + }) + setDependenciesMap(map) + } + + const generateNewPermit:(data:RolePermissionItem[])=>RolePermissionItem[] = (data:RolePermissionItem[]) =>{ + return data.map((item:RolePermissionItem)=>({ + ...item,children:item.children.map((child:PermissionClassify)=>({ + ...child, + children:child.children.map((permission:PermissionItem & {dependents:string[]})=>({ + ...permission, value:`${roleType}.${item.value}.${child.value}.${permission.value}` + })) + })) + })) + } + + const getPermissionTemplate = ()=>{ + return fetchData>(`${roleType}/role/template`,{method:'GET'}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + const newPermits = generateNewPermit(data.permits) + generateDependenciesMap(newPermits) + setPermissionTemplate(newPermits) + console.log(newPermits) + }else{ + console.log(message) + message.error(msg || '获取权限模板失败') + } + }) + } + + const getPermissionInfo = ()=>{ + fetchData>(`${roleType}/role`,{method:'GET',eoParams:{role:roleId}}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + form.setFieldsValue({name:data.role.name,permits:data.role.permit}) + return Promise.resolve(true) + }else{ + message.error(msg || '操作失败') + return Promise.reject(msg || '操作失败') + } + }).catch((errInfo)=>Promise.reject(errInfo)) + } + + useEffect(() => { + getPermissionTemplate() + form.setFieldsValue({name:'',permits:[]}) + if(roleId){ + getPermissionInfo() + } + }, []); + + const onFinish =async() => { + const body = await form.validateFields() + + return fetchData>(`${roleType}/role`,{method:roleId === undefined? 'POST' : 'PUT',eoBody:({...body}),...(roleId !== undefined?{eoParams:{role:roleId}}:{})}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + return Promise.resolve(true) + }else{ + message.error(msg || '操作失败') + return Promise.reject(msg || '操作失败') + } + }).catch((errInfo)=>Promise.reject(errInfo)) + }; + + return (
+
+ +
+ +
+
+ + + + + + + + {APP_MODE === 'pro' &&
+ + + + +
} +
+ +
+
) +} +export default RoleConfig \ No newline at end of file diff --git a/frontend/packages/core/src/pages/role/RoleList.tsx b/frontend/packages/core/src/pages/role/RoleList.tsx new file mode 100644 index 00000000..1af89ffc --- /dev/null +++ b/frontend/packages/core/src/pages/role/RoleList.tsx @@ -0,0 +1,161 @@ +import { App, Divider} from "antd"; +import PageList from "@common/components/aoplatform/PageList.tsx"; +import { useEffect, useRef,} from "react"; +import {ActionType, ProColumns} from "@ant-design/pro-components"; +import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import { ROLE_TABLE_COLUMNS } from "../../const/role/const.tsx"; +import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx"; +import { PERMISSION_DEFINITION } from "@common/const/permissions.ts"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; +import { checkAccess } from "@common/utils/permission.ts"; +import { useNavigate } from "react-router-dom"; +import { RoleTableListItem } from "@core/const/role/type.ts"; + + +const RoleList = ()=>{ + const { modal,message } = App.useApp() + const { setBreadcrumb } = useBreadcrumb() + const {fetchData} = useFetch() + const pageListRef = useRef(null); + const {accessData} = useGlobalContext() + const navigateTo = useNavigate() + + const operation:(type:string)=>ProColumns[] =(type:string)=>[ + // TODO 开源版隐藏操作 + { + title: '操作', + key: 'option', + width: 93, + fixed:'right', + valueType: 'option', + render: (_: React.ReactNode, entity: RoleTableListItem) => [ + {navigateTo(`/role/${type}/config/${entity.id}`)}} btnTitle="查看"/>, + // {navigateTo(`/role/${type}/config/${entity.id}`)}} btnTitle="编辑"/>, + // , + // {openModal(type as 'system'|'team','delete',entity)}} btnTitle="删除"/>, + ], + } + ] + + const getRoleList = (group:'team'|'system')=>{ + return fetchData>(`${group}/roles`,{method:'GET'}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + return {data:data.roles, success: true} + }else{ + message.error(msg || '操作失败') + return {data:[], success:false} + } + }).catch(() => { + return {data:[], success:false} + }) + } + + const deleteRole = (entity:RoleTableListItem)=>{ + return new Promise((resolve, reject)=>{ + fetchData>(`manage/role`,{method:'DELETE',eoParams:{id:entity.id}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + const manualReloadTable = () => { + pageListRef.current?.reload() + }; + + const isActionAllowed = (accessType:'system'|'team', type:'add'|'edit'|'delete') => { + + const permission = `system.organization.role.${accessType}.${type}` as keyof typeof PERMISSION_DEFINITION[0] ; + + return !checkAccess(permission, accessData); + }; + + const openModal = (accessType:'system'|'team', type:'delete',entity?:RoleTableListItem)=>{ + let title:string = '' + let content:string|React.ReactNode = '' + switch (type){ + case 'delete': + title='删除' + content='该数据删除后将无法找回,请确认是否删除?' + break; + } + + modal.confirm({ + title, + content, + onOk:()=>{ + switch (type){ + case 'delete': + return deleteRole(entity!).then((res)=>{if(res === true) manualReloadTable()}) + } + }, + width:600, + okText:'确认', + okButtonProps:{ + disabled:isActionAllowed(accessType, type) + }, + cancelText:'取消', + closable:true, + icon:<>, + }) + } + + useEffect(() => { + setBreadcrumb([ + { + title: '角色'}]) + }, []); + + return (<> +
+
+

角色

+

设置角色的权限范围。

+
+

系统级别角色

+ [], ...operation('system')]} + request={()=>getRoleList('system')} + addNewBtnTitle="添加角色" + showPagination={false} + onAddNewBtnClick={() => { + navigateTo(`/role/system/config`) + }} + noScroll={true} + addNewBtnAccess="system.organization.role.system.add" + onRowClick={(row:RoleTableListItem)=> navigateTo(`/role/system/config/${row.id}`)} + tableClickAccess="system.organization.role.system.edit" + /> +

团队级别角色

+ [], ...operation('team')]} + request={()=>getRoleList('team')} + showPagination={false} + addNewBtnTitle="添加角色" + onAddNewBtnClick={() => { + navigateTo(`/role/team/config`) + }} + noScroll={true} + addNewBtnAccess="system.organization.role.team.add" + onRowClick={(row:RoleTableListItem)=> navigateTo(`/role/team/config/${row.id}`)} + tableClickAccess="system.organization.role.team.edit" + /> +
+ ) +} +export default RoleList; \ No newline at end of file diff --git a/frontend/packages/core/src/pages/serviceCategory/ServiceCategory.tsx b/frontend/packages/core/src/pages/serviceCategory/ServiceCategory.tsx new file mode 100644 index 00000000..60449f2a --- /dev/null +++ b/frontend/packages/core/src/pages/serviceCategory/ServiceCategory.tsx @@ -0,0 +1,272 @@ +import TreeWithMore from "@common/components/aoplatform/TreeWithMore"; +import WithPermission from "@common/components/aoplatform/WithPermission"; +import { BasicResponse, STATUS_CODE } from "@common/const/const"; +import { PERMISSION_DEFINITION } from "@common/const/permissions"; +import { useFetch } from "@common/hooks/http"; +import { checkAccess } from "@common/utils/permission"; +import { CategorizesType, ServiceHubCategoryConfigHandle } from "@market/const/serviceHub/type"; +import { App, Button, Spin, TagType, Tree, TreeDataNode, TreeProps } from "antd"; +import { DataNode } from "antd/es/tree"; +import { Key, useEffect, useMemo, useRef, useState } from "react"; +import { ServiceHubCategoryConfig } from "./ServiceHubCategoryConfig"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext"; +import { useBreadcrumb } from "@common/contexts/BreadcrumbContext"; +import { LoadingOutlined } from "@ant-design/icons"; +import { cloneDeep } from "lodash-es"; +import { Icon } from "@iconify/react/dist/iconify.js"; + +export default function ServiceCategory(){ + const [gData, setGData] = useState([]); + const [cateData, setCateData] = useState([]); + const [expandedKeys, setExpandedKeys] = useState([]); + const {message,modal} = App.useApp() + const {fetchData} = useFetch() + const addRef = useRef(null) + const addChildRef = useRef(null) + const renameRef = useRef(null) + const {accessData} = useGlobalContext() + const { setBreadcrumb } = useBreadcrumb() + const [loading, setLoading] = useState(false) + + const onDrop: TreeProps['onDrop'] = (info) => { + const dropKey = info.node.key; + const dragKey = info.dragNode.key; + const dropPos = info.node.pos.split('-'); + const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]); // the drop position relative to the drop node, inside 0, top -1, bottom 1 + + const loop = ( + data: TreeDataNode[], + key: React.Key, + callback: (node: TreeDataNode, i: number, data: TreeDataNode[]) => void, + ) => { + for (let i = 0; i < data.length; i++) { + if (data[i].id === key) { + return callback(data[i], i, data); + } + if (data[i].children) { + loop(data[i].children!, key, callback); + } + } + }; + const data = cloneDeep(gData); + + // Find dragObject + let dragObj: TreeDataNode; + loop(data, dragKey, (item, index, arr) => { + arr.splice(index, 1); + dragObj = item; + }); + + if (!info.dropToGap) { + // Drop on the content + loop(data, dropKey, (item) => { + item.children = item.children || []; + // where to insert. New item was inserted to the start of the array in this example, but can be anywhere + item.children.unshift(dragObj); + }); + } else { + let ar: TreeDataNode[] = []; + let i: number; + loop(data, dropKey, (_item, index, arr) => { + ar = arr; + i = index; + }); + if (dropPosition === -1) { + // Drop on the top of the drop node + ar.splice(i!, 0, dragObj!); + } else { + // Drop on the bottom of the drop node + ar.splice(i! + 1, 0, dragObj!); + } + } + + setGData(data); + sortCategories(data) + }; + + + const dropdownMenu = (entity:CategorizesType) => [ + { + key: 'addChildCate', + label: ( + + ), + }, + { + key: 'renameCate', + label: ( + + ), + }, + { + key: 'delete', + label: ( + + ), + }, + ]; + + const treeData = useMemo(() => { + setExpandedKeys([]) + const loop = (data: CategorizesType[]): DataNode[] => + data?.map((item) => { + if (item.children) { + setExpandedKeys(prev=>[...prev,item.id]) + return { + title: {item.name} , + key: item.id, children: loop(item.children) + }; + } + + return { + title: {item.name}, + key: item.id, + }; + }); + return loop(gData ?? []) + }, [gData]); + + const isActionAllowed = (type:'addCate'|'addChildCate'|'renameCate'|'delete') => { + const actionToPermissionMap = { + 'addCate': 'add', + 'addChildCate': 'add', + 'renameCate': 'edit', + 'delete': 'delete' + }; + + const action = actionToPermissionMap[type]; + const permission :keyof typeof PERMISSION_DEFINITION[0]= `system.api_market.service_classification.${action}`; + + return !checkAccess(permission, accessData); + }; + + const openModal = (type:'addCate'|'addChildCate'|'renameCate'|'delete',entity?:CategorizesType)=>{ + let title:string = '' + let content:string|React.ReactNode = '' + switch (type){ + case 'addCate': + title='添加分类' + content= + break; + case 'addChildCate': + title='添加子分类' + content= + break; + case 'renameCate': + title='重命名分类' + content= + break; + case 'delete': + title='删除' + content='该数据删除后将无法找回,请确认是否删除?' + break; + } + modal.confirm({ + title, + content, + onOk:()=>{ + switch (type){ + case 'addCate': + return addRef.current?.save().then((res)=>{if(res === true) getCategoryList()}) + case 'addChildCate': + return addChildRef.current?.save().then((res)=>{if(res === true) getCategoryList()}) + case 'renameCate': + return renameRef.current?.save().then((res)=>{if(res === true) getCategoryList()}) + case 'delete': + return deleteCate(entity!).then((res)=>{if(res === true) getCategoryList()}) + } + }, + width:600, + okText:'确认', + okButtonProps:{ + disabled : isActionAllowed(type) + }, + cancelText:'取消', + closable:true, + icon:<>, + }) + } + + const deleteCate = (entity:CategorizesType)=>{ + return new Promise((resolve, reject)=>{ + fetchData>('catalogue',{method:'DELETE',eoParams:{catalogue:entity.id},}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功,即将刷新页面') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + const sortCategories = (newData:CategorizesType[])=>{ + setLoading(true) + fetchData>('catalogue/sort',{method:'PUT',eoBody:newData}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + getCategoryList() + }else{ + setGData(cateData) + message.error(msg || '操作失败') + } + }).catch(()=>{setGData(cateData)}).finally(()=>{setLoading(false)}) + } + + const getCategoryList = ()=>{ + setLoading(true) + fetchData>('catalogues',{method:'GET'}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setGData(data.catalogues) + setCateData(data.catalogues) + }else{ + message.error(msg || '操作失败') + } + }).finally(()=>{setLoading(false)}) + } + + useEffect(()=>{ + setBreadcrumb([ + { + title: '服务分类管理'}]) + getCategoryList() + },[]) + + return ( +
+
+

服务分类管理

+

设置服务可选择的分类,方便团队成员快速找到API。

+
+
+ } spinning={loading} className=''> + {setExpandedKeys(expandedKeys as string[])}} + onDrop={onDrop} + treeData={treeData} + /> + + + + +
+
+ ) +} \ No newline at end of file diff --git a/frontend/packages/core/src/pages/serviceCategory/ServiceHubCategoryConfig.tsx b/frontend/packages/core/src/pages/serviceCategory/ServiceHubCategoryConfig.tsx new file mode 100644 index 00000000..b4e31eaa --- /dev/null +++ b/frontend/packages/core/src/pages/serviceCategory/ServiceHubCategoryConfig.tsx @@ -0,0 +1,125 @@ +import {App, Form, Input} from "antd"; +import {forwardRef, useEffect, useImperativeHandle} from "react"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import { ServiceHubCategoryConfigHandle, ServiceHubCategoryConfigFieldType, ServiceHubCategoryConfigProps } from "@market/const/serviceHub/type.ts" +import WithPermission from "@common/components/aoplatform/WithPermission"; + +export const ServiceHubCategoryConfig = forwardRef((props,ref)=>{ + const { message } = App.useApp() + const [form] = Form.useForm(); + const {type,entity} = props + const {fetchData} = useFetch() + + const save:()=>Promise = ()=>{ + const url:string = 'catalogue' + let method:string + switch (type){ + case 'addCate': + case 'addChildCate': + method = 'POST' + break; + case 'renameCate': + method = 'PUT' + break + } + return new Promise((resolve, reject)=>{ + if(!url || !method){ + reject('类型错误') + return + } + form.validateFields().then((value)=>{ + fetchData>(url,{method,eoBody:(value), eoParams:{ ...(type === 'renameCate' ? {catalogue:value.id} :undefined)}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + useImperativeHandle(ref, ()=>({ + save + }) + ) + + useEffect(() => { + + switch(type){ + case 'addCate': + //console.log(entity) + form.setFieldsValue({}) + break + case 'addChildCate': + form.setFieldsValue({parent:entity!.id}) + break + case 'renameCate': + //console.log(entity) + form.setFieldsValue(entity) + break + } + + }, []); + + + return ( + +
+ + {type === 'renameCate' && + + label="ID" + name="id" + hidden + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + + } + {(type === 'addCate' || type === 'renameCate') && + + label="分类名称" + name="name" + rules={[{ required: true, message: '必填项' ,whitespace:true }]} + > + + } + + {type === 'addChildCate' &&<> + + label="父分类 ID" + name="parent" + hidden + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + + + + label="子分类名称" + name="name" + rules={[{ required: true, message: '必填项' ,whitespace:true }]} + > + + + + } + +
+) +}) \ No newline at end of file diff --git a/frontend/packages/core/src/pages/system/SystemConfig.tsx b/frontend/packages/core/src/pages/system/SystemConfig.tsx new file mode 100644 index 00000000..af588182 --- /dev/null +++ b/frontend/packages/core/src/pages/system/SystemConfig.tsx @@ -0,0 +1,394 @@ + +import {forwardRef, useEffect, useImperativeHandle, useState} from "react"; +import {App, Button, Divider, Form, Input, Radio, Row, Select, TagType, TreeSelect, Upload} from "antd"; +import { Link, useNavigate, useParams} from "react-router-dom"; +import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import {DefaultOptionType} from "antd/es/cascader"; +import { EntityItem, MemberItem, SimpleTeamItem} from "@common/const/type.ts"; +import { v4 as uuidv4 } from 'uuid' +import { SystemConfigFieldType, SystemConfigHandle } from "../../const/system/type.ts"; +import { validateUrlSlash } from "@common/utils/validate.ts"; +import { compressImage, normFile } from "@common/utils/uploadPic.ts"; +import { useBreadcrumb } from "@common/contexts/BreadcrumbContext.tsx"; +import { useSystemContext } from "../../contexts/SystemContext.tsx"; +import { visualizations } from "@core/const/system/const.tsx"; +import { RcFile, UploadChangeParam, UploadFile, UploadProps } from "antd/es/upload/interface"; +import { LoadingOutlined } from "@ant-design/icons"; +import { getImgBase64 } from "@common/utils/dataTransfer.ts"; +import { CategorizesType } from "@market/const/serviceHub/type.ts"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import { Icon } from "@iconify/react/dist/iconify.js"; + +const MAX_SIZE = 2 * 1024; // 1KB + +const SystemConfig = forwardRef((_,ref) => { + const { message,modal } = App.useApp() + const { teamId, serviceId } = useParams(); + const [onEdit, setOnEdit] = useState(!!teamId) + const [form] = Form.useForm(); + const {fetchData} = useFetch() + const [teamOptionList, setTeamOptionList] = useState() + const navigate = useNavigate(); + const {setBreadcrumb} = useBreadcrumb() + const { setSystemInfo} = useSystemContext() + const [showClassify, setShowClassify] = useState() + const [imageBase64, setImageBase64] = useState(null); + const [tagOptionList, setTagOptionList] = useState([]) + const [serviceClassifyOptionList, setServiceClassifyOptionList] = useState() + const [uploadLoading, setUploadLoading] = useState(false) + + useImperativeHandle(ref, () => ({ + save:onFinish + })); + + const beforeUpload = async (file: RcFile) => { + if (!['image/png', 'image/jpeg', 'image/svg+xml'].includes(file.type)) { + alert('只允许上传PNG、JPG或SVG格式的图片'); + return false; + } + + if (file.size > MAX_SIZE) { + try { + const compressedBase64 = await compressImage(file, MAX_SIZE); + setImageBase64(`data:${file.type};base64,${compressedBase64}`); + form.setFieldValue('logo', `data:${file.type};base64,${compressedBase64}`); + } catch (error) { + console.error('压缩图片时出错', error); + } + } else { + const reader = new FileReader(); + reader.onload = (e: ProgressEvent) => { + setImageBase64(e.target?.result as string); + form.setFieldValue('logo', e.target?.result); + }; + reader.readAsDataURL(file); + } + return false; + }; + + + const handleChange: UploadProps['onChange'] = (info: UploadChangeParam) => { + if (info.file.status === 'uploading') { + setUploadLoading(true); + return; + } + if (info.file.status === 'done') { + getImgBase64(info.file.originFileObj as RcFile, () => { + setUploadLoading(false); + }); + } + if (info.fileList.length === 0) { + form.setFieldValue( "logo", null ); + } + }; + + const uploadButton = ( +
+ {uploadLoading ? : } +
+ ); + + const getTagAndServiceClassifyList = ()=>{ + setTagOptionList([]) + setServiceClassifyOptionList([]) + fetchData>('catalogues',{method:'GET'}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setTagOptionList(data.tags?.map((x:TagType)=>{return { + label:x.name, value:x.name + }})||[]) + setServiceClassifyOptionList(data.catalogues) + + }else{ + message.error(msg || '操作失败') + } + }) + } + + // 获取表单默认值 + const getSystemInfo = () => { + fetchData>('service/info',{method:'GET',eoParams:{team:teamId, service:serviceId},eoTransformKeys:['team_id','service_type']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setTimeout(()=>{ + form.setFieldsValue({ + ...data.service, + team:data.service.team.id, + catalogue:data.service.catalogue?.id, + tags:data.service.tags?.map((x:EntityItem)=>x.id), + logoFile:[ + { + uid: '-1', // 文件唯一标识 + name: 'image.png', // 文件名 + status: 'done', // 状态有:uploading, done, error, removed + url: data.service?.logo || '', // 图片 Base64 数据 + } + ] + }) + console.log({ + ...data.service, + team:data.service.team.id, + catalogue:data.service.catalogue?.id, + logoFile:[ + { + uid: '-1', // 文件唯一标识 + name: 'image.png', // 文件名 + status: 'done', // 状态有:uploading, done, error, removed + url: data.service?.logo || '', // 图片 Base64 数据 + } + ] + }) + setImageBase64(data.service.logo) + setShowClassify(data.service.serviceType === 'public') + },0) + }else{ + message.error(msg || '操作失败') + } + }) + }; + + const onFinish:()=>Promise = () => { + return form.validateFields().then((value)=>{ + return fetchData>(serviceId === undefined? 'team/service':'service/info',{method:serviceId === undefined? 'POST' : 'PUT',eoParams: {...(serviceId === undefined ? {team:value.team} :{service:serviceId,team:teamId})},eoBody:({...value,prefix:value.prefix?.trim()}), eoTransformKeys:['serviceType']},).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + setSystemInfo(data.service) + return Promise.resolve(true) + }else{ + message.error(msg || '操作失败') + return Promise.reject(msg || '操作失败') + } + }).catch((errorInfo)=>{ + return Promise.reject(errorInfo) + }) + }) + }; + + + const getTeamOptionList = ()=>{ + setTeamOptionList([]) + fetchData>('simple/teams/mine',{method:'GET',eoTransformKeys:['available_partitions']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setTeamOptionList(data.teams?.map((x:MemberItem)=>{return {...x, + label:x.name, value:x.id + }})) + }else{ + message.error(msg || '操作失败') + } + }) + } + + const deleteSystem = ()=>{ + fetchData>('team/service',{method:'DELETE',eoParams:{team:teamId,service:serviceId}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + navigate(`/service/list`) + }else{ + message.error(msg || '操作失败') + } + }) + } + + useEffect(() => { + getTeamOptionList() + getTagAndServiceClassifyList() + if (serviceId !== undefined) { + setOnEdit(true); + getSystemInfo(); + setBreadcrumb([ + { + title: 内部数据服务 + }, + { + title: '设置' + }]) + + } else { + setOnEdit(false); + form.setFieldValue('id',uuidv4()); + form.setFieldValue('team',teamId); + form.setFieldValue('serviceType','inner'); + } + return (form.setFieldsValue({})) + }, [serviceId]); + + + const deleteSystemModal = async ()=>{ + modal.confirm({ + title:'删除', + content:'该数据删除后将无法找回,请确认是否删除?', + onOk:()=> { + return deleteSystem() + }, + width:600, + okText:'确认', + okButtonProps:{ + danger:true + }, + cancelText:'取消', + closable:true, + icon:<> + }) + } + + return ( + <> +
+ +
+
+ + label="服务名称" + name="name" + rules={[{ required: true, message: '必填项' ,whitespace:true }]} + > + + + + + label="服务ID" + name="id" + extra="服务ID(sys_id)可用于检索服务或日志" + rules={[{ required: true, message: '必填项' ,whitespace:true }]} + > + + + + + label="API 调用前缀" + name="prefix" + extra="选填,作为服务内所有服务的API的前缀,比如host/{sys_name}/{service_name}/{api_path},一旦保存无法修改" + rules={[ + { + validator: validateUrlSlash, + }]} + > + + + + + label="图标" + name="logoFile" + extra="仅支持 .png .jpg .jpeg .svg 格式的图片文件, 大于 1KB 的文件将被压缩" + valuePropName="fileList" getValueFromEvent={normFile} + > + +
+ {imageBase64 ? Logo : uploadButton} +
+
+ + + + + label="描述" + name="description" + > + + + + + label="Logo" + name="logo" + hidden + > + + + {!onEdit && + label="所属团队" + name="team" + rules={[{ required: true, message: '必填项' }]} + > + + } + + + + label="标签" + name="tags" + > + + + + + label="服务类型" + name="serviceType" + rules={[{required: true, message: '必填项'}]} + > + {setShowClassify(e.target.value === 'public')}} /> + + + {showClassify && + + label="所属服务分类" + name="catalogue" + extra="设置服务展示在服务市场中的哪个分类下" + rules={[{required: true, message: '必填项'}]} + > + + + } + {onEdit && <> + + + + + } +
+ {onEdit && <> +
+

删除服务:删除操作不可恢复,请谨慎操作!

+
+ + + +
+
+ } + +
+
+ + ) +}) +export default SystemConfig \ No newline at end of file diff --git a/frontend/packages/core/src/pages/system/SystemInsideDocument.tsx b/frontend/packages/core/src/pages/system/SystemInsideDocument.tsx new file mode 100644 index 00000000..83c34a6b --- /dev/null +++ b/frontend/packages/core/src/pages/system/SystemInsideDocument.tsx @@ -0,0 +1,146 @@ +import { Editor } from '@tinymce/tinymce-react'; +import hljs from 'highlight.js'; +import 'highlight.js/styles/default.css'; +import {useEffect, useState} from "react"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import {App, Button} from "antd"; +import { EntityItem } from '@common/const/type.ts'; +import WithPermission from '@common/components/aoplatform/WithPermission.tsx'; +import { RouterParams } from '@core/components/aoplatform/RenderRoutes'; +import { useParams } from 'react-router-dom'; +const ServiceInsideDocument = ()=>{ + const { message } = App.useApp() + const [serviceName,setServiceName] = useState() + const [updater,setUpdater] = useState() + const [updateTime,setUpdateTime]=useState() + const [initDoc, setInitDoc] = useState() + const [doc, setDoc] = useState() + const {fetchData} = useFetch() + const { serviceId, teamId} = useParams(); + + const save = ()=>{ + fetchData>('service/doc',{method:'PUT',eoBody:({doc:doc}) ,eoParams:{service:serviceId,team:teamId},eoTransformKeys:['update_time']}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + getServiceDoc() + }else{ + message.error(msg || '操作失败') + } + }) + } + + const handleEditorChange = (content:string, editor:unknown) => { + setDoc(content) + }; + const setupEditor = (editor:unknown) => { + editor.on('init', () => { + editor.contentDocument.querySelectorAll('pre code').forEach((block:HTMLElement) => { + hljs.highlightBlock(block); + }); + }); + + editor.on('SetContent', () => { + editor.contentDocument.querySelectorAll('pre code').forEach((block:HTMLElement) => { + hljs.highlightBlock(block); + }); + }); + }; + + const getServiceDoc = ()=>{ + fetchData>('service/doc',{method:'GET',eoParams:{service:serviceId,team:teamId},eoTransformKeys:['update_time']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setServiceName(data.doc.name) + setUpdater(data.doc.updater.id === '' ? '-' : data.doc.updater.name) + setUpdateTime(data.doc.updater.id === '' ? '-' : data.doc.updateTime) + setInitDoc(data.doc.doc) + }else{ + message.error(msg || '操作失败') + } + }) + } + + useEffect(() => { + getServiceDoc() + }, []); + + return ( +
+ + +
+
+

最近一次更新者:{updater || '-'}最近一次更新时间:{updateTime || '-'}

+ +
+
+
) +} +export default ServiceInsideDocument \ No newline at end of file diff --git a/frontend/packages/core/src/pages/system/SystemInsidePage.tsx b/frontend/packages/core/src/pages/system/SystemInsidePage.tsx new file mode 100644 index 00000000..774d5c7e --- /dev/null +++ b/frontend/packages/core/src/pages/system/SystemInsidePage.tsx @@ -0,0 +1,131 @@ + +import {FC, useEffect, useMemo, useState} from "react"; +import {Outlet, useLocation, useNavigate, useParams} from "react-router-dom"; +import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx"; +import {App, Menu, MenuProps} from "antd"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import { useSystemContext} from "../../contexts/SystemContext.tsx"; +import { SYSTEM_PAGE_MENU_ITEMS } from "../../const/system/const.tsx"; +import { SystemConfigFieldType } from "../../const/system/type.ts"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; +import { PERMISSION_DEFINITION } from "@common/const/permissions.ts"; +import InsidePage from "@common/components/aoplatform/InsidePage.tsx"; +import Paragraph from "antd/es/typography/Paragraph"; +import { ItemType, MenuItemGroupType, MenuItemType } from "antd/es/menu/hooks/useItems"; +import { cloneDeep } from "lodash-es"; + +const SystemInsidePage:FC = ()=> { + const { message } = App.useApp() + const { teamId,serviceId,apiId} = useParams(); + const location = useLocation() + const currentUrl = location.pathname + const {fetchData} = useFetch() + const { setPrefixForce,setApiPrefix ,systemInfo,setSystemInfo} = useSystemContext() + const { accessData,checkPermission} = useGlobalContext() + const [activeMenu, setActiveMenu] = useState() + const navigateTo = useNavigate() + + const getSystemInfo = ()=>{ + fetchData>('service/info',{method:'GET',eoParams:{team:teamId, service:serviceId}}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setSystemInfo(data.service) + }else{ + message.error(msg || '操作失败') + } + }) + } + + const getApiDefine = ()=>{ + setApiPrefix('') + setPrefixForce(false) + fetchData>('service/api/define',{method:'GET',eoParams:{service:serviceId,team:teamId}}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setApiPrefix(data.prefix) + setPrefixForce(data.force) + }else{ + message.error(msg || '操作失败') + } + }) + } + + const menuData = useMemo(()=>{ + const filterMenu = (menu:MenuItemGroupType[])=>{ + const newMenu = cloneDeep(menu) + return newMenu!.filter((m:MenuItemGroupType )=>{ + if(m.children && m.children.length > 0){ + m.children = m.children.filter( + (c)=>(c&&(c as MenuItemType&{access:string} ).access ? + checkPermission((c as MenuItemType&{access:string} ).access as keyof typeof PERMISSION_DEFINITION[0]): + true)) + } + return m.children && m.children.length > 0 + }) + } + const filteredMenu = filterMenu(SYSTEM_PAGE_MENU_ITEMS as MenuItemGroupType[]) + setActiveMenu((pre)=>{ + return pre ?? 'api' + }) + return filteredMenu || [] + },[accessData]) + + const onMenuClick: MenuProps['onClick'] = ({key}) => { + setActiveMenu(key) + }; + + useEffect(() => { + console.log(apiId, serviceId, currentUrl) + if(apiId !== undefined){ + setActiveMenu('api') + }else if(serviceId !== currentUrl.split('/')[currentUrl.split('/').length - 1]){ + setActiveMenu(currentUrl.split('/')[currentUrl.split('/').length - 1]) + }else{ + setActiveMenu('api') + } + console.log(activeMenu) + }, [currentUrl]); + + useEffect(()=>{ + if(accessData && accessData.get('team') && accessData.get('team')?.indexOf('team.service.api.view') !== -1){ + getApiDefine() + } + },[accessData]) + + useEffect(()=>{ + if( activeMenu && serviceId === currentUrl.split('/')[currentUrl.split('/').length - 1]){ + navigateTo(`/service/${teamId}/inside/${serviceId}/${activeMenu}`) + } + },[activeMenu]) + + useEffect(() => { + serviceId && getSystemInfo() + }, [serviceId]); + + return ( + <> + 服务 ID:{serviceId || '-'} + }]} + backUrl="/service/list"> +
+ [] } + /> +
+ +
+
+
+ + + ) +} +export default SystemInsidePage \ No newline at end of file diff --git a/frontend/packages/core/src/pages/system/SystemInsideSubscriber.tsx b/frontend/packages/core/src/pages/system/SystemInsideSubscriber.tsx new file mode 100644 index 00000000..d04123df --- /dev/null +++ b/frontend/packages/core/src/pages/system/SystemInsideSubscriber.tsx @@ -0,0 +1,288 @@ +import {ActionType, ProColumns} from "@ant-design/pro-components"; +import {FC, forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState} from "react"; +import {Link, useParams} from "react-router-dom"; +import {App, Form, Select,TreeSelect} from "antd"; +import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import {useFetch} from "@common/hooks/http.ts"; +import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import PageList from "@common/components/aoplatform/PageList.tsx"; +import {DefaultOptionType} from "antd/es/cascader"; +import { SYSTEM_SUBSCRIBER_TABLE_COLUMNS } from "../../const/system/const.tsx"; +import { SystemSubscriberTableListItem, SystemSubscriberConfigFieldType, SystemSubscriberConfigHandle, SystemSubscriberConfigProps, SimpleSystemItem } from "../../const/system/type.ts"; +import { NewSimpleMemberItem, SimpleMemberItem } from "@common/const/type.ts"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; +import { checkAccess } from "@common/utils/permission.ts"; + +const SystemInsideSubscriber:FC = ()=>{ + const { setBreadcrumb } = useBreadcrumb() + const { modal,message } = App.useApp() + const {fetchData} = useFetch() + const [init, setInit] = useState(true) + const {serviceId, teamId} = useParams() + const addRef = useRef(null) + const pageListRef = useRef(null); + const [memberValueEnum, setMemberValueEnum] = useState<{[k:string]:{text:string}}>({}) + const {accessData} = useGlobalContext() + const getSystemSubscriber = ()=>{ + return fetchData>('service/subscribers',{method:'GET',eoParams:{service:serviceId,team:teamId},eoTransformKeys:['apply_time']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setInit((prev)=>prev ? false : prev) + return {data:data.subscribers, success: true} + }else{ + message.error(msg || '操作失败') + return {data:[], success:false} + } + }).catch(() => { + return {data:[], success:false} + }) + } + + const getMemberList = async ()=>{ + setMemberValueEnum({}) + const {code,data,msg} = await fetchData>('simple/member',{method:'GET'}) + if(code === STATUS_CODE.SUCCESS){ + const tmpValueEnum:{[k:string]:{text:string}} = {} + data.members?.forEach((x:SimpleMemberItem)=>{ + tmpValueEnum[x.name] = {text:x.name} + }) + setMemberValueEnum(tmpValueEnum) + }else{ + message.error(msg || '操作失败') + } + } + + const manualReloadTable = () => { + pageListRef.current?.reload() + }; + + const deleteSubscriber = (entity:SystemSubscriberTableListItem)=>{ + return new Promise((resolve, reject)=>{ + fetchData>('service/subscriber',{method:'DELETE',eoParams:{application:entity!.id,service:entity!.service.id,team:teamId}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + const openModal =async (type:'delete'|'add',entity?:SystemSubscriberTableListItem)=>{ + let title:string = '' + let content:string|React.ReactNode = '' + switch (type){ + case 'add': + title='新增订阅方' + content= + break; + case 'delete': + title='删除' + content='该数据删除后将无法找回,请确认是否删除?' + break; + } + + modal.confirm({ + title, + content, + onOk:()=>{ + switch (type){ + case 'add': + return addRef.current?.save().then((res)=>{if(res === true) manualReloadTable()}) + case 'delete': + return deleteSubscriber(entity!).then((res)=>{if(res === true) manualReloadTable()}) + } + }, + width:600, + okText:'确认', + okButtonProps:{ + disabled : !checkAccess( `team.service.subscription.${type}`, accessData) + }, + cancelText:'取消', + closable:true, + icon:<>, + }) + } + + const operation:ProColumns[] =[ + { + title: '操作', + key: 'option', + width: 62, + fixed:'right', + valueType: 'option', + render: (_: React.ReactNode, entity: SystemSubscriberTableListItem) => [ + {openModal('delete',entity)}} btnTitle="删除"/>, + ], + } + ] + + useEffect(() => { + setBreadcrumb([ + { + title:内部数据服务 + }, + { + title:'订阅方管理' + } + ]) + getMemberList() + manualReloadTable() + }, [serviceId]); + + const columns = useMemo(()=>{ + return SYSTEM_SUBSCRIBER_TABLE_COLUMNS.map(x=>{if(x.filters &&((x.dataIndex as string[])?.indexOf('applier') !== -1 || (x.dataIndex as string[])?.indexOf('approver') !== -1) ){x.valueEnum = memberValueEnum} return x}) + },[memberValueEnum]) + + return ( + getSystemSubscriber()} + // dataSource={tableListDataSource} + showPagination={false} + addNewBtnTitle="新增订阅方" + onAddNewBtnClick={()=>{openModal('add')}} + addNewBtnAccess="team.service.subscription.add" + /> + ) +} + +export default SystemInsideSubscriber + + +export const SystemSubscriberConfig = forwardRef((props, ref) => { + const { message } = App.useApp() + const { serviceId, teamId} = props + const [form] = Form.useForm(); + const {fetchData} = useFetch() + const [systemOptionList, setSystemOptionList] = useState() + const [memberOptionList, setMemberOptionList] = useState() + const [subscriberTeamId, setSubscriberTeamId] = useState() + const save:()=>Promise = ()=>{ + return new Promise((resolve, reject)=>{ + form.validateFields().then((value)=>{ + fetchData>('service/subscriber',{method:'POST',eoBody:({...value,service:serviceId}), eoParams:{service:serviceId,team:teamId}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }) + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + useImperativeHandle(ref, ()=>({ + save + }) + ) + + + const getSystemList = ()=>{ + setSystemOptionList([]) + fetchData>('simple/apps/mine',{method:'GET'}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + const teamMap = new Map(); + + data.apps + .filter((x:SimpleSystemItem)=>x.id !== serviceId) + .forEach((item:SimpleSystemItem) => { + if (!teamMap.has(item.team.id)) { + teamMap.set(item.team.id, { + title: item.team.name, + value: item.team.id, + key: item.team.id, + children: [], + selectable: false, // 第一级不可选 + disabled:true + }); + } + + teamMap.get(item.team.id)!.children!.push({ + title: item.name, + value: item.id, + key: item.id, + selectable: true, // 子级可选 + // partition:item.partition?.map((x:EntityItem)=>x.id) || [] + }); + }); + setSystemOptionList(Array.from(teamMap.values())) + }else{ + message.error(msg || '操作失败') + } + }) + } + + useEffect(()=>{ + subscriberTeamId && getMemberList() + form.setFieldValue('applier',null) + },[subscriberTeamId]) + + const getMemberList = ()=>{ + setMemberOptionList([]) + fetchData>('team/members/simple',{method:'GET',eoParams:{team:subscriberTeamId}}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setMemberOptionList(data.members?.map((x:NewSimpleMemberItem)=>{return { + label:x.user.name, value:x.user.id + }})) + }else{ + message.error(msg || '操作失败') + } + }) + } + + useEffect(() => { + getSystemList() + }, [serviceId]); + + return ( +
+ + label="订阅方" + name="subscriber" + rules={[{ required: true, message: '必填项' }]} + > + {setSubscriberTeamId(_)}} + /> + + + + + + + + label="描述" + name="description" + > + + + + + label="请求方式" + name="method" + rules={[{ required: true, message: '必填项' }]} + > + + + + + label="请求路径" + name="path" + rules={[{ required: true, message: '必填项',whitespace:true }, + { + validator: validateUrlSlash, + }]} + className={styles['form-input-group']} + > + + + + + label="高级匹配" + name="match" + > + + configFields={MATCH_CONFIG} + /> + + {/* } */} + + { type !== 'copy' &&<> + +
转发规则设置 + + className="mb-0 bg-transparent border-none p-0" + name="proxy" + > + + + } + + + + ) +}) +export default SystemInsideApiCreate \ No newline at end of file diff --git a/frontend/packages/core/src/pages/system/api/SystemInsideApiDetail.tsx b/frontend/packages/core/src/pages/system/api/SystemInsideApiDetail.tsx new file mode 100644 index 00000000..1e06ad1e --- /dev/null +++ b/frontend/packages/core/src/pages/system/api/SystemInsideApiDetail.tsx @@ -0,0 +1,98 @@ + +import {useEffect, useRef, useState} from "react"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import {App, Button, Spin} from "antd"; +import ApiBasicInfoDisplay from "@common/components/postcat/api/ApiPreview/components/ApiBasicInfoDisplay"; +import ApiPreview from "@common/components/postcat/ApiPreview.tsx"; +import ApiMatch from "@common/components/postcat/api/ApiPreview/components/ApiMatch"; +import {v4 as uuidv4} from 'uuid' +import ApiProxy from "@common/components/postcat/api/ApiPreview/components/ApiProxy"; +import { ProxyHeaderItem, SystemApiDetail, SystemInsideApiDetailProps, SystemInsideApiDocumentHandle } from "../../../const/system/type.ts"; +import { MatchItem } from "@common/const/type.ts"; +import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter.tsx"; +import SystemInsideApiDocument from "./SystemInsideApiDocument.tsx"; +import ScrollableSection from "@common/components/aoplatform/ScrollableSection.tsx"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import { LoadingOutlined } from "@ant-design/icons"; + +const SystemInsideApiDetail = (props:SystemInsideApiDetailProps)=>{ + const { message } = App.useApp() + const {serviceId, teamId, apiId} = props + const {fetchData} = useFetch() + const [apiDetail, setApiDetail] = useState() + const [open, setOpen] = useState(false); + const drawerFormRef = useRef(null) + const [loading, setLoading] = useState(false) + + const getApiDetail = ()=>{ + setLoading(true) + fetchData>('service/api/detail',{method:'GET',eoParams:{service:serviceId,team:teamId, api:apiId},eoTransformKeys:['create_time','update_time','match_type','upstream_id','opt_type']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + const newApiDetail = { + ...data.api, + match:data.api.match?.map((x:MatchItem)=>{x.id = x.id ?? uuidv4();return x}) || [], + ...data.api.proxy && {proxy:{...data.api.proxy, + headers:data.api.proxy?.headers?.map((x:ProxyHeaderItem)=>{x.id = x.id?? uuidv4();return x || [] + })} + } + } + setApiDetail(newApiDetail) + }else{ + message.error(msg || '操作失败') + } + }).finally(()=>{setLoading(false)}) + } + + const onClose = ()=>{ + setOpen(false) + } + + useEffect(() => { + getApiDetail() + }, []); + + return ( + } spinning={loading} className="h-full 1" rootClassName="h-full 2" wrapperClassName="h-full 3" > +
+ +
+ { + apiDetail !== undefined && <> +
+ + +
+

+ 创建者:{apiDetail?.creator.name || '-'} + 最后编辑人:{apiDetail?.updater.name || '-'}更新时间:{apiDetail?.updateTime || '-'}

+ } +
+
+ { + apiDetail?.match && apiDetail.match?.length > 0 && + + } + + { + apiDetail?.proxy && Object.keys(apiDetail?.proxy).length > 0 && + + } + + {apiDetail && } +
+
+ drawerFormRef.current?.save()?.then((res)=>{res&& getApiDetail();return res})} + showLastStep={true} + > + + +
+
) +} +export default SystemInsideApiDetail \ No newline at end of file diff --git a/frontend/packages/core/src/pages/system/api/SystemInsideApiDocument.tsx b/frontend/packages/core/src/pages/system/api/SystemInsideApiDocument.tsx new file mode 100644 index 00000000..8f1cd5e7 --- /dev/null +++ b/frontend/packages/core/src/pages/system/api/SystemInsideApiDocument.tsx @@ -0,0 +1,66 @@ + +import {forwardRef, useEffect, useImperativeHandle, useRef, useState} from "react"; +import ApiEdit, {ApiEditApi} from "@common/components/postcat/ApiEdit.tsx"; +import { Spin, message} from "antd"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import { SystemApiDetail, SystemInsideApiDocumentHandle, SystemInsideApiDocumentProps } from "../../../const/system/type.ts"; +import { LoadingOutlined } from "@ant-design/icons"; + + +const SystemInsideApiDocument = forwardRef((props, ref) => { + const {serviceId, teamId, apiId} = props + const {fetchData} = useFetch() + const [apiDetail, setApiDetail] = useState() + const apiEditRef = useRef(null) + const [loaded,setLoaded] = useState(false) + const [loading, setLoading] = useState(false) + + useImperativeHandle(ref, ()=>({ + save + }) +) + useEffect(() => { + getApiDetail() + }, []); + + const getApiDetail = ()=>{ + setLoading(true) + fetchData>('service/api/detail',{method:'GET',eoParams:{service:serviceId,team:teamId, api:apiId},eoTransformKeys:['create_time','update_time','match_type','upstream_id','opt_type']}).then(response=>{ + const {code,data,msg} = response + //console.log(data,code, STATUS_CODE.SUCCESS,code === STATUS_CODE.SUCCESS) + if(code === STATUS_CODE.SUCCESS){ + setApiDetail(data.api) + setLoaded(true) + }else{ + message.error(msg || '操作失败') + } + }).finally(()=>{setLoading(false)}) + } + + const save = ()=>{ + return apiEditRef.current?.getData()?.then((res)=>{ + return fetchData>('service/api',{method:'PUT',eoParams:{service:serviceId,team:teamId,api:apiId},eoBody:(res.apiInfo)}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功') + return Promise.resolve(true) + }else{ + message.error(msg || '操作失败') + return Promise.reject(msg|| '操作失败') + } + }).catch(errInfo => Promise.reject(errInfo)) + }) + + } + + return (<> + } spinning={loading} className=' h-full overflow-auto '> +
+ +
+
+ ) +}) + +export default SystemInsideApiDocument \ No newline at end of file diff --git a/frontend/packages/core/src/pages/system/api/SystemInsideApiList.tsx b/frontend/packages/core/src/pages/system/api/SystemInsideApiList.tsx new file mode 100644 index 00000000..ef21f228 --- /dev/null +++ b/frontend/packages/core/src/pages/system/api/SystemInsideApiList.tsx @@ -0,0 +1,245 @@ +import PageList from "@common/components/aoplatform/PageList.tsx" +import {ActionType, ProColumns} from "@ant-design/pro-components"; +import {FC, useEffect, useMemo, useRef, useState} from "react"; +import {Link, useParams} from "react-router-dom"; +import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import {App, Divider} from "antd"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import { SimpleMemberItem} from '@common/const/type.ts' +import {useFetch} from "@common/hooks/http.ts"; +import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx"; +import SystemInsideApiCreate from "./SystemInsideApiCreate.tsx"; +import {useSystemContext} from "../../../contexts/SystemContext.tsx"; +import { SYSTEM_API_TABLE_COLUMNS } from "../../../const/system/const.tsx"; +import { SystemApiSimpleFieldType, SystemApiTableListItem, SystemInsideApiCreateHandle, SystemInsideApiDocumentHandle } from "../../../const/system/type.ts"; +import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; +import { checkAccess } from "@common/utils/permission.ts"; +import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter.tsx"; +import SystemInsideApiDetail from "./SystemInsideApiDetail.tsx"; +import SystemInsideApiDocument from "./SystemInsideApiDocument.tsx"; + +const SystemInsideApiList:FC = ()=>{ + const [searchWord, setSearchWord] = useState('') + const { setBreadcrumb } = useBreadcrumb() + const { modal,message } = App.useApp() + // const [confirmLoading, setConfirmLoading] = useState(false); + const [init, setInit] = useState(true) + const [tableListDataSource, setTableListDataSource] = useState([]); + const [tableHttpReload, setTableHttpReload] = useState(true); + const {fetchData} = useFetch() + const pageListRef = useRef(null); + const copyRef = useRef(null) + const {apiPrefix, prefixForce} = useSystemContext() + const [memberValueEnum, setMemberValueEnum] = useState<{[k:string]:{text:string}}>({}) + const {accessData} = useGlobalContext() + const [drawerType,setDrawerType]= useState<'add'|'edit'|'view'|'upstream'|undefined>() + const [open, setOpen] = useState(false); + const drawerEditFormRef = useRef(null) + const drawerAddFormRef = useRef(null) + const {serviceId, teamId} = useParams() + + const [curApi, setCurApi] = useState() + + const getApiList = (): Promise<{ data: SystemApiTableListItem[], success: boolean }>=> { + //console.log(sorter, filter) + if(!tableHttpReload){ + setTableHttpReload(true) + return Promise.resolve({ + data: tableListDataSource, + success: true, + }); + } + + return fetchData>('service/apis',{method:'GET',eoParams:{service:serviceId,team:teamId, keyword:searchWord},eoTransformKeys:['request_path','create_time','update_time','can_delete']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setTableListDataSource(data.apis) + setInit((prev)=>prev ? false : prev) + setTableHttpReload(false) + return {data:data.apis, success: true} + }else{ + message.error(msg || '操作失败') + return {data:[], success:false} + } + }).catch(() => { + return {data:[], success:false} + }) + } + + const deleteApi = (entity:SystemApiTableListItem)=>{ + return new Promise((resolve, reject)=>{ + fetchData>('service/api',{method:'DELETE',eoParams:{service:serviceId,team:teamId, api:entity!.id}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + const openModal = async (type:'copy' | 'delete',entity:SystemApiTableListItem) =>{ + let title:string = '' + let content:string|React.ReactNode = '' + switch (type){ + case 'copy':{ + title='复制 API' + message.loading('正在加载数据') + const {code,data,msg} = await fetchData>('service/api/detail/simple',{method:'GET',eoParams:{service:serviceId,team:teamId, api:entity!.id}}) + message.destroy() + if(code === STATUS_CODE.SUCCESS){ + content= + }else{ + message.error(msg || '操作失败') + return + } + break;} + case 'delete': + title='删除' + content='该数据删除后将无法找回,请确认是否删除?' + break; + } + + modal.confirm({ + title, + content, + onOk:()=> { + switch (type){ + case 'copy': + return copyRef.current?.copy().then(()=> { + manualReloadTable() + }) + case 'delete': + return deleteApi(entity).then((res)=>{if(res === true) manualReloadTable()}) + } + }, + width:type==='copy'? 900: 600, + okText:'确认', + okButtonProps:{ + disabled : !checkAccess( `team.service.api.${type}`, accessData ) + }, + cancelText:'取消', + closable:true, + icon:<>, + }) + } + + const operation:ProColumns[] =[ + { + title: '操作', + key: 'option', + width: 194, + fixed:'right', + valueType: 'option', + render: (_: React.ReactNode, entity: SystemApiTableListItem) => [ + {openDrawer('view',entity)}} btnTitle="详情"/>, + , + {openModal('copy',entity)}} btnTitle="复制"/>, + , + {openDrawer('edit',entity)}} btnTitle="编辑"/>, + entity.canDelete && , + entity.canDelete && {openModal('delete',entity)}} btnTitle="删除"/>, + ], + } + ] + + const manualReloadTable = () => { + setTableHttpReload(true); // 表格数据需要从后端接口获取 + pageListRef.current?.reload() + }; + + const getMemberList = async ()=>{ + setMemberValueEnum({}) + const {code,data,msg} = await fetchData>('simple/member',{method:'GET'}) + if(code === STATUS_CODE.SUCCESS){ + const tmpValueEnum:{[k:string]:{text:string}} = {} + data.members?.forEach((x:SimpleMemberItem)=>{ + tmpValueEnum[x.name] = {text:x.name} + }) + setMemberValueEnum(tmpValueEnum) + }else{ + message.error(msg || '操作失败') + } + } + + const openDrawer = (type:'add'|'edit'|'view',entity?:SystemApiTableListItem)=>{ + setCurApi(entity) + setDrawerType(type) + } + + useEffect(()=>{drawerType !== undefined ? setOpen(true):setOpen(false)},[drawerType]) + + useEffect(() => { + setBreadcrumb([ + { + title:内部数据服务 + }, + { + title:'API' + } + ]) + getMemberList() + manualReloadTable() + }, [serviceId]); + + const onClose = () => { + setDrawerType(undefined); + setCurApi(undefined) + }; + + const columns = useMemo(()=>{ + return SYSTEM_API_TABLE_COLUMNS.map(x=>{if(x.filters &&((x.dataIndex as string[])?.indexOf('creator') !== -1) ){x.valueEnum = memberValueEnum} return x}) + },[memberValueEnum]) + + const handlerSubmit:() => Promise|undefined= ()=>{ + switch(drawerType){ + case 'add':{ + return drawerAddFormRef.current?.save()?.then((res)=>{res && manualReloadTable();return res}) + } + case 'edit':{ + return drawerEditFormRef.current?.save()?.then((res)=>{res && manualReloadTable();return res}) + } + default:return undefined + } + } + + return ( + <> + getApiList()} + dataSource={tableListDataSource} + addNewBtnTitle="添加 API" + searchPlaceholder="输入名称、URL 查找 API" + onAddNewBtnClick={()=>{openDrawer('add')}} + addNewBtnAccess="team.service.api.add" + tableClickAccess="team.service.api.view" + manualReloadTable={manualReloadTable} + onSearchWordChange={(e)=>{setSearchWord(e.target.value)}} + onChange={() => { + setTableHttpReload(false) + }} + onRowClick={(row:SystemApiTableListItem)=>openDrawer('view',row)} + /> + handlerSubmit()} + showOkBtn={drawerType !== 'view'} + > + {drawerType === 'add' && } + {drawerType === 'edit' && } + {drawerType === 'view' && } + + + ) + +} +export default SystemInsideApiList \ No newline at end of file diff --git a/frontend/packages/core/src/pages/system/api/SystemInsideApiPlugin.tsx b/frontend/packages/core/src/pages/system/api/SystemInsideApiPlugin.tsx new file mode 100644 index 00000000..e88d6938 --- /dev/null +++ b/frontend/packages/core/src/pages/system/api/SystemInsideApiPlugin.tsx @@ -0,0 +1,7 @@ +import {FC} from "react"; + +const SystemInsideApiPlugin:FC = ()=>{ + + return (<>) +} +export default SystemInsideApiPlugin \ No newline at end of file diff --git a/frontend/packages/core/src/pages/system/api/SystemInsideApiProxy.tsx b/frontend/packages/core/src/pages/system/api/SystemInsideApiProxy.tsx new file mode 100644 index 00000000..c372a7ff --- /dev/null +++ b/frontend/packages/core/src/pages/system/api/SystemInsideApiProxy.tsx @@ -0,0 +1,77 @@ + +import { Form, Input, InputNumber } from "antd"; +import { forwardRef, useEffect, useImperativeHandle } from "react" +import EditableTableWithModal from "@common/components/aoplatform/EditableTableWithModal"; +import { PROXY_HEADER_CONFIG } from "../../../const/system/const"; +import { SystemApiProxyType, ProxyHeaderItem, SystemInsideApiProxyHandle, SystemInsideApiProxyProps } from "../../../const/system/type"; + +const SystemInsideApiProxy = forwardRef((props,ref)=>{ + const {value, onChange, className,initProxyValue} = props + + const [form] = Form.useForm(); + + useEffect(()=>{ + initProxyValue && form.setFieldsValue({ + ...initProxyValue, + path:initProxyValue.path ? (initProxyValue.path.startsWith('/')? initProxyValue.path.substring(1):initProxyValue.path):''}) + },[]) + + useImperativeHandle(ref,()=>({ + validate:form.validateFields + })) + + useEffect(() => { + form.setFieldsValue(value) + }, [value,form]); + + return ( + <> +
{onChange?.(allValues)}} + autoComplete="off"> + + + label="转发上游路径" + name={'path'} + > + + + + + label="请求超时时间" + name={'timeout'} + extra="单位:ms,最小值:1" + rules={[{required: true, message: '必填项'}]} + > + + + + + label="重试次数" + name={'retry'} + rules={[{required: true, message: '必填项'}]} + > + + + + + label="转发上游请求头" + name={'headers'} + > + + configFields={PROXY_HEADER_CONFIG} + /> + + + + ) +}) +export default SystemInsideApiProxy \ No newline at end of file diff --git a/frontend/packages/core/src/pages/system/approval/SystemInsideApproval.module.css b/frontend/packages/core/src/pages/system/approval/SystemInsideApproval.module.css new file mode 100644 index 00000000..55cfe008 --- /dev/null +++ b/frontend/packages/core/src/pages/system/approval/SystemInsideApproval.module.css @@ -0,0 +1,9 @@ +:global .ant-tabs.ant-tabs-top{ + height:100%; + .ant-tabs-content.ant-tabs-content-top{ + height:100%; + .ant-tabs-tabpane.ant-tabs-tabpane-active{ + height:100%; + } + } +} \ No newline at end of file diff --git a/frontend/packages/core/src/pages/system/approval/SystemInsideApproval.tsx b/frontend/packages/core/src/pages/system/approval/SystemInsideApproval.tsx new file mode 100644 index 00000000..6279f556 --- /dev/null +++ b/frontend/packages/core/src/pages/system/approval/SystemInsideApproval.tsx @@ -0,0 +1,32 @@ + +import {Tabs} from "antd"; +import {Outlet, useLocation, useNavigate} from "react-router-dom"; +import './SystemInsideApproval.module.css' +import {FC, useEffect, useState} from "react"; +import { SYSTEM_INSIDE_APPROVAL_TAB_ITEMS } from "../../../const/system/const"; + + +const SystemInsideApproval:FC = ()=>{ + const navigateTo = useNavigate() + const location = useLocation() + const query =new URLSearchParams(useLocation().search) + const currentUrl = location.pathname + const [pageStatus,setPageStatus] = useState<0|1>(Number(query.get('status') ||0) as 0|1) + const onChange = (key: string) => { + setPageStatus(Number(key) as 0|1) + navigateTo(`${currentUrl}?status=${key}`); + }; + + useEffect(() => { + setPageStatus(Number(query.get('status') ||0) as 0|1) + }, [currentUrl]); + + return ( + <> + + + + ) +} + +export default SystemInsideApproval \ No newline at end of file diff --git a/frontend/packages/core/src/pages/system/approval/SystemInsideApprovalList.tsx b/frontend/packages/core/src/pages/system/approval/SystemInsideApprovalList.tsx new file mode 100644 index 00000000..17118d89 --- /dev/null +++ b/frontend/packages/core/src/pages/system/approval/SystemInsideApprovalList.tsx @@ -0,0 +1,189 @@ + +import {ActionType, ProColumns} from "@ant-design/pro-components"; +import {FC, useEffect, useMemo, useRef, useState} from "react"; +import {Link, useLocation, useParams} from "react-router-dom"; +import PageList from "@common/components/aoplatform/PageList.tsx"; +import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import {App, Button} from "antd"; +import { + SUBSCRIBE_APPROVAL_INNER_DONE_TABLE_COLUMN, + SUBSCRIBE_APPROVAL_INNER_TODO_TABLE_COLUMN, + SubscribeApprovalTableListItem, TODO_LIST_COLUMN_NOT_INCLUDE_KEY +} from "@common/const/approval/const.tsx"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx"; +import { + SubscribeApprovalModalContent, + SubscribeApprovalModalHandle +} from "@common/components/aoplatform/SubscribeApprovalModalContent.tsx"; +import {useSystemContext} from "../../../contexts/SystemContext.tsx"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import { EntityItem,SimpleMemberItem } from "@common/const/type.ts"; +import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; +import { checkAccess } from "@common/utils/permission.ts"; +import { SubscribeApprovalInfoType } from "@common/const/approval/type.tsx"; + +const SystemInsideApprovalList:FC = ()=>{ + const { setBreadcrumb } = useBreadcrumb() + const { modal,message } = App.useApp() + const {serviceId, teamId} = useParams(); + const [init, setInit] = useState(true) + const {fetchData} = useFetch() + const [tableHttpReload, setTableHttpReload] = useState(true); + const [tableListDataSource, setTableListDataSource] = useState([]); + const pageListRef = useRef(null); + const query =new URLSearchParams(useLocation().search) + const [pageStatus,setPageStatus] = useState<0|1>(Number(query.get('status') ||0) as 0|1) + const subscribeRef = useRef(null) + const [approvalBtnLoading,setApprovalBtnLoading] = useState(false) + const [memberValueEnum, setMemberValueEnum] = useState<{[k:string]:{text:string}}>({}) + const {accessData} = useGlobalContext() + + const openModal = async (type:'approval'|'view',entity:SubscribeApprovalTableListItem)=>{ + message.loading('正在加载数据') + const {code,data,msg} = await fetchData>('service/approval/subscribe',{method:'GET',eoParams:{apply:entity!.id, service:serviceId,team:teamId},eoTransformKeys:['apply_project','apply_team','apply_time','approval_time']}) + message.destroy() + if(code === STATUS_CODE.SUCCESS){ + const modalIns = modal.confirm({ + title:type === 'approval' ? '审批' : '查看', + content:, + onOk:()=>{ + return subscribeRef.current?.save('pass').then((res)=>res === true && manualReloadTable()) + }, + width:600, + okText:type === 'approval' ? '通过' : '确认', + cancelText:type === 'approval' ?'取消':'关闭', + okButtonProps:{ + disabled : type === 'approval' ? !checkAccess('team.service.release.approval', accessData): false + }, + closable:true, + onCancel:()=>{setApprovalBtnLoading(false)}, + icon:<>, + footer:(_, { OkBtn, CancelBtn }) =>{ + return ( + <> + {type === 'approval' ? <> + + + + : + <> + + {/* */} + + } + + ) + }, + }) + }else{ + message.error(msg || '操作失败') + return + } + } + + const operation:ProColumns[] =[ + { + title: '操作', + key: 'option', + width: 62, + fixed:'right', + valueType: 'option', + render: (_: React.ReactNode, entity: SubscribeApprovalTableListItem) => [ + pageStatus === 0 ? + {openModal('approval',entity)}} btnTitle="审批"/> + :{openModal('view',entity)}} btnTitle="查看"/>, + ], + } + ] + + const getApprovalList = ()=>{ + if(!tableHttpReload){ + setTableHttpReload(true) + return Promise.resolve({ + data: tableListDataSource, + success: true, + }); + } + return fetchData>('service/approval/subscribes',{method:'GET',eoParams:{service:serviceId,team:teamId, status:(query.get('status') || 0)},eoTransformKeys:['apply_time','apply_project','approval_time']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setTableListDataSource(data.approvals) + setInit((prev)=>prev ? false : prev) + return {data:data.approvals, success: true} + }else{ + message.error(msg || '操作失败') + return {data:[], success:false} + } + }).catch(() => { + return {data:[], success:false} + }) + } + + const getMemberList = async ()=>{ + setMemberValueEnum({}) + const {code,data,msg} = await fetchData>('simple/member',{method:'GET'}) + if(code === STATUS_CODE.SUCCESS){ + const tmpValueEnum:{[k:string]:{text:string}} = {} + data.members?.forEach((x:SimpleMemberItem)=>{ + tmpValueEnum[x.name] = {text:x.name} + }) + setMemberValueEnum(tmpValueEnum) + }else{ + message.error(msg || '操作失败') + } + } + + useEffect(() => { + !init && pageListRef.current?.reload() + }, [pageStatus]); + + + useEffect(() => { + setPageStatus(Number(query.get('status') ||0) as 0|1) + }, [query]); + + useEffect(() => { + setBreadcrumb([ + { + title:内部数据服务 + }, + { + title:'订阅审批' + } + ]) + getMemberList() + manualReloadTable() + }, [serviceId]); + + const manualReloadTable = () => { + setTableHttpReload(true); // 表格数据需要从后端接口获取 + pageListRef.current?.reload() + }; + + + const columns = useMemo(()=>{ + const newCol = [...(!(query.get('status'))? SUBSCRIBE_APPROVAL_INNER_TODO_TABLE_COLUMN:SUBSCRIBE_APPROVAL_INNER_DONE_TABLE_COLUMN)] + const filteredCol = pageStatus === 0 ? newCol.filter((x)=>TODO_LIST_COLUMN_NOT_INCLUDE_KEY.indexOf(x.dataIndex as string) === -1): newCol + return filteredCol.map(x=>{if(x.filters &&((x.dataIndex as string[])?.indexOf('applier') !== -1 || (x.dataIndex as string[])?.indexOf('approver') !== -1) ){x.valueEnum = memberValueEnum} return x}) + },[pageStatus,memberValueEnum]) + + return ( +
+ getApprovalList()} + onChange={() => { + setTableHttpReload(false) + }} + onRowClick={(row:SubscribeApprovalTableListItem)=>openModal(pageStatus === 0 ? 'approval': 'view',row)} + tableClickAccess={pageStatus === 0 ?'team.service.subscription.approval':'team.service.subscription.view'} + /> +
+ ) +} +export default SystemInsideApprovalList \ No newline at end of file diff --git a/frontend/packages/core/src/pages/system/publish/SystemInsidePublish.tsx b/frontend/packages/core/src/pages/system/publish/SystemInsidePublish.tsx new file mode 100644 index 00000000..0fc84fac --- /dev/null +++ b/frontend/packages/core/src/pages/system/publish/SystemInsidePublish.tsx @@ -0,0 +1,46 @@ + +import { Tabs } from "antd" +import { useState, useEffect, FC } from "react" +import { Link, Outlet, useLocation, useNavigate, useParams } from "react-router-dom" +import { useBreadcrumb } from "@common/contexts/BreadcrumbContext" +import { RouterParams } from "@core/components/aoplatform/RenderRoutes" +import { SYSTEM_PUBLISH_TAB_ITEMS } from "../../../const/system/const" + +const SystemInsidePublic:FC = ()=>{ + const { setBreadcrumb } = useBreadcrumb() + const query =new URLSearchParams(useLocation().search) + const location = useLocation() + const currentUrl = location.pathname + const [pageStatus,setPageStatus] = useState<0|1>(Number(query.get('status') ||0) as 0|1) + const navigateTo = useNavigate() + const { teamId} = useParams(); + + const onChange = (key: string) => { + setPageStatus(Number(key) as 0|1) + navigateTo(`${currentUrl}?status=${key}`); + }; + + useEffect(() => { + setPageStatus(Number(query.get('status') ||0) as 0|1) + }, [currentUrl]); + + useEffect(() => { + setBreadcrumb([ + { + title:内部数据服务 + }, + { + title:'发布' + } + ]) + }, []); + + return ( + <> + + + + ) + +} +export default SystemInsidePublic \ No newline at end of file diff --git a/frontend/packages/core/src/pages/system/publish/SystemInsidePublishList.tsx b/frontend/packages/core/src/pages/system/publish/SystemInsidePublishList.tsx new file mode 100644 index 00000000..178c47bf --- /dev/null +++ b/frontend/packages/core/src/pages/system/publish/SystemInsidePublishList.tsx @@ -0,0 +1,500 @@ +import { ActionType, ParamsType, ProColumns } from "@ant-design/pro-components"; +import { App, Button, Divider } from "antd"; +import { useState, useRef, useEffect, useMemo, FC } from "react"; +import { useParams, Link, useLocation } from "react-router-dom"; +import PageList from "@common/components/aoplatform/PageList"; +import { PublishApprovalModalHandle, PublishApprovalModalContent } from "@common/components/aoplatform/PublishApprovalModalContent"; +import { RouterParams } from "@core/components/aoplatform/RenderRoutes"; +import { PUBLISH_APPROVAL_RECORD_INNER_TABLE_COLUMN, PUBLISH_APPROVAL_VERSION_INNER_TABLE_COLUMN } from "@common/const/approval/const"; +import { BasicResponse, STATUS_CODE } from "@common/const/const"; +import { SimpleMemberItem } from "@common/const/type.ts"; +import { MemberTableListItem } from "../../../const/member/type"; +import { useBreadcrumb } from "@common/contexts/BreadcrumbContext"; +import { useFetch } from "@common/hooks/http"; +import WithPermission from "@common/components/aoplatform/WithPermission"; +import { SystemPublishReleaseItem } from "../../../const/system/type"; +import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext"; +import { PERMISSION_DEFINITION } from "@common/const/permissions"; +import { checkAccess } from "@common/utils/permission"; +import SystemInsidePublishOnline from "./SystemInsidePublishOnline"; +import { PublishVersionTableListItem, PublishTableListItem, PublishApprovalInfoType } from "@common/const/approval/type"; +import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter"; + +const SystemInsidePublicList:FC = ()=>{ + const { setBreadcrumb } = useBreadcrumb() + const { modal,message } = App.useApp() + const pageListRef = useRef(null); + const [tableHttpReload, setTableHttpReload] = useState(true); + const [init, setInit] = useState(true) + const {fetchData} = useFetch() + const [tableListDataSource, setTableListDataSource] = useState([]); + const {serviceId, teamId} = useParams(); + const drawerRef = useRef(null) + // const approvalRef = useRef(null) + // const addRef = useRef(null) + // const onlineRef = useRef(null) + const [extraModalBtnLoading,setExtraModalBtnLoading] = useState(false) + const [pageStatus,setPageStatus] = useState<0|1>(0 as 0|1) + const [pageType, setPageType] = useState<'insideSystem'|'global'>('insideSystem') + const query =new URLSearchParams(useLocation().search) + const currLocation = useLocation().pathname + const [memberValueEnum, setMemberValueEnum] = useState<{[k:string]:{text:string}}>({}) + const {accessData} = useGlobalContext() + const [drawerTitle, setDrawerTitle] = useState('') + const [drawerType, setDrawerType] = useState<'approval'|'view'|'add'|'publish'|'online'>('view') + const [drawerVisible, setDrawerVisible] = useState(false) + const [drawerData, setDrawerData] = useState({} as PublishTableListItem) + const [drawerOkTitle, setDrawerOkTitle] = useState('确认') + const [isOkToPublish, setIsOkToPublish] = useState(false) + const getSystemPublishList = (params?: ParamsType & { + pageSize?: number | undefined; + current?: number | undefined; + keyword?: string | undefined; + })=>{ + if(!(pageType !== 'insideSystem' && pageStatus !== 0 ) && !tableHttpReload){ + setTableHttpReload(true) + return Promise.resolve({ + data: tableListDataSource, + success: true, + }); + } + return fetchData>( + pageStatus === 0 ? 'service/releases':'service/publishs', + {method:'GET',eoParams:(pageType !== 'insideSystem' && pageStatus !== 0 ) ? {service:serviceId,team:teamId,page:params?.current,page_size:params?.pageSize}:{service:serviceId,team:teamId},eoTransformKeys:['pageSize','apply_time','approve_time','release_status','is_valid','fail_msg','create_time','can_rollback','flow_id','can_delete']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + const finalRes = pageStatus === 0 ? data.releases.map((x:PublishVersionTableListItem)=>{if(!x.status|| x.status === 'close'){x.status = 'none'} return x}):data.publishs + setTableListDataSource(finalRes) + setInit((prev)=>prev ? false : prev) + return {data:finalRes, success: true} + }else{ + message.error(msg || '操作失败') + setInit((prev)=>prev ? false : prev) + return {data:[], success:false} + } + }).catch(() => { + return {data:[], success:false} + }) + } + + const handlePublishAction = (type:'rollback'|'delete'|'stop',entity:PublishTableListItem | PublishVersionTableListItem)=>{ + let url:string ='service/release' + let method:string + let params:{[k:string]:unknown} = {} + switch(type){ + case 'rollback': + method = 'POST' + params = {service:serviceId,team:teamId, id:entity.id} + break; + case 'delete': + method = 'DELETE' + params = {service:serviceId,team:teamId,id:entity.id} + break; + case 'stop': + url = 'service/publish/stop' + method = 'DELETE' + params = {service:serviceId,team:teamId,id:(entity as PublishVersionTableListItem).flowId} + break; + } + + return new Promise((resolve, reject)=>{ + fetchData>(url,{method,eoParams:params}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + })} + + + const isActionAllowed = (type:'view' | 'delete' | 'add' |'stop'|'online'|'cancel'|'approval' | 'rollback'|'publish') => { + const permission :keyof typeof PERMISSION_DEFINITION[0]= `team.service.release.${type === 'publish'? 'add' : type}`; + return !checkAccess(permission, accessData); + }; + + const handleOnline = (entity:PublishTableListItem | PublishVersionTableListItem)=>{ + modal.confirm({ + title:'发布结果', + content:, + width: 600, + closable: true, + wrapClassName:'ant-modal-without-footer', + icon: <>, + footer:null, + onCancel:()=>{ + manualReloadTable() + } + }); + } + + const openDrawer = async(type: 'view' | 'add'|'online'|'approval'|'publish', entity?: PublishTableListItem|PublishVersionTableListItem)=>{ + setIsOkToPublish(false) + switch (type) { + case 'view':{ + message.loading('正在加载数据'); + const viewPublish:boolean = pageStatus !== 0 || ((entity as PublishVersionTableListItem)?.status && (entity as PublishVersionTableListItem)?.status !== 'none') + const { code, data, msg } = await fetchData>( + viewPublish ? 'service/publish':'service/release', + { method: 'GET', eoParams:{id: (entity as PublishVersionTableListItem)?.[viewPublish && pageStatus === 0 ? 'flowId':'id'],service:serviceId,team:teamId },eoTransformKeys:['cluster_publish_status','upstream_status','doc_status','proxy_status','version_remark'] } + ); + message.destroy(); + if (code === STATUS_CODE.SUCCESS) { + setDrawerTitle('查看详情') + setDrawerType(type) + setDrawerData(viewPublish ? data.publish : data.release)} else { + message.error(msg || '操作失败'); + return + } + break; + } + case 'online':{ + message.loading('正在加载数据'); + const { code, data, msg } = await fetchData>( + 'service/publish', + { method: 'GET', eoParams:{ id: (entity as PublishVersionTableListItem)?.flowId,service:serviceId,team:teamId },eoTransformKeys:['version_remark'] } + ); + message.destroy(); + if (code === STATUS_CODE.SUCCESS) { + setDrawerTitle('上线') + setDrawerType(type) + setDrawerOkTitle('上线') + setDrawerData({...data.publish, flowId:(entity as PublishVersionTableListItem)?.flowId}) + } else { + message.error(msg || '操作失败'); + return + } + break; + } + case 'approval':{ + message.loading('正在加载数据'); + const { code, data, msg } = await fetchData>( + 'service/publish', + { method: 'GET', eoParams:{ id: (entity as PublishVersionTableListItem)?.flowId,service:serviceId,team:teamId },eoTransformKeys:['version_remark'] } + ); + message.destroy(); + if (code === STATUS_CODE.SUCCESS) { + setDrawerTitle('审批') + setDrawerType(type) + setDrawerData(data.publish) + setDrawerOkTitle('通过') + } else { + message.error(msg || '操作失败'); + return + } + break; + } + case 'publish': + case 'add':{ + message.loading('正在加载数据'); + const { code, data, msg } = await fetchData>( + 'service/publish/check', + { method: 'GET', eoParams:{service:serviceId,team:teamId, ...(type === 'publish' ?{ release:entity?.id }:{})},eoTransformKeys:['version_remark'] } + ); + message.destroy(); + if (code === STATUS_CODE.SUCCESS) { + setDrawerTitle('申请发布') + setDrawerType(type) + setDrawerData({...data, ...(type === 'publish'&& {version:entity?.version, id:entity?.id})}) + setDrawerOkTitle('确认') + setIsOkToPublish(data.isOk??true) + } else { + message.error(msg || '操作失败'); + return + } + break; + } + } + setDrawerVisible(true) + } + + + const openModal = async (type: 'delete' |'stop'|'cancel' | 'rollback', entity?: PublishTableListItem|PublishVersionTableListItem) => { + let title: string = ''; + let content: string | React.ReactNode = ''; + switch (type) { + case 'delete': + title = '删除'; + content = '该数据删除后将无法找回,请确认是否删除?'; + break; + case 'rollback': + title = '回滚'; + content = '请确认是否回滚?'; + break; + case 'cancel': + title = '撤销申请'; + content = '请确认是否撤销申请?'; + break; + case 'stop': + title = '终止发布'; + content = '请确认是否终止发布?'; + break; + } + + modal.confirm({ + title, + content, + onOk: () => { + switch (type){ + case 'rollback': + return handlePublishAction('rollback',entity!).then((res)=>{if(res === true)manualReloadTable()}) + case 'delete': + return handlePublishAction('delete',entity!).then((res)=>{if(res === true)manualReloadTable()}) + case 'cancel': + case 'stop': + return handlePublishAction('stop',entity!).then((res)=>{if(res === true)manualReloadTable()}) + } + }, + width: 600, + okText: '确认', + cancelText: '取消', + onCancel:()=>{setExtraModalBtnLoading(false)}, + closable: true, + icon: <>, + okButtonProps:{ + disabled: isActionAllowed(type) || false + }, + footer: (_, { OkBtn, CancelBtn }) => ( + <> + + + + + + ), + }); + }; + + const tableOperation = (entity:PublishTableListItem | PublishVersionTableListItem)=>{ + const viewBtn = {openDrawer('view',entity)}} btnTitle="查看详情"/> + let btnArr:React.ReactNode[] = [] + if(pageType !== 'insideSystem' && pageStatus !== 0){ + btnArr = [ + viewBtn + ] + return btnArr + } + + if((entity as PublishVersionTableListItem).status === 'accept'){ + btnArr = [ + {openDrawer('online',entity)}} btnTitle="上线"/>, + , + viewBtn, + , + {openModal('stop',entity)}} btnTitle="终止发布"/> + ] + } + + + if((entity as PublishVersionTableListItem).status === 'publishing'){ + btnArr = [ + viewBtn, + , + {openModal('stop',entity)}} btnTitle="终止发布"/> + ] + } + + if((entity as PublishVersionTableListItem).status === 'apply'){ + btnArr = [ + {openDrawer('approval',entity)}} btnTitle="审批"/>, + , + viewBtn, + , + {openModal('cancel',entity)}} btnTitle="撤回申请"/> + ] + } + + // 第一期不做回滚 + // if( (entity as PublishVersionTableListItem).status === 'online' && (entity as PublishVersionTableListItem).canRollback){ + // btnArr = [...btnArr, + // ...(btnArr.length > 0 ? []: + // [viewBtn, + // ]), + // + // ] + // } + + if( ['close','refuse','none'].indexOf((entity as PublishVersionTableListItem).status as string) !== -1 || !(entity as PublishVersionTableListItem).flowId){ + btnArr = [...btnArr, + ...(btnArr.length > 0 ? []: + [viewBtn, + // + ]), + // {openDrawer('publish',entity)}} btnTitle="申请发布"/> + ] + } + + if( ['running','error'].indexOf((entity as PublishVersionTableListItem).status as string) !== -1 && (entity as PublishVersionTableListItem).flowId){ + btnArr = [viewBtn] + } + + if((entity as PublishVersionTableListItem).canDelete){ + btnArr = [...btnArr, btnArr.length > 0 && ,{openModal('delete',entity)}} btnTitle="删除"/> ] + } + + return btnArr + + } + + const operation:ProColumns[] =[ + { + title: '操作', + key: 'option', + width:pageStatus === 0 ? 231 : 93, + valueType: 'option', + fixed:'right', + render: (_: React.ReactNode, entity: PublishTableListItem|PublishVersionTableListItem) => tableOperation(entity) + } + ] + + useEffect(() => { + setBreadcrumb([ + { + title:内部数据服务 + }, + { + title:'发布' + } + ]) + getMemberList() + manualReloadTable() + }, [serviceId]); + + + const getMemberList = async ()=>{ + setMemberValueEnum({}) + const {code,data,msg} = await fetchData>('simple/member',{method:'GET'}) + if(code === STATUS_CODE.SUCCESS){ + const tmpValueEnum:{[k:string]:{text:string}} = {} + data.members?.forEach((x:SimpleMemberItem)=>{ + tmpValueEnum[x.name] = {text:x.name} + }) + setMemberValueEnum(tmpValueEnum) + }else{ + message.error(msg || '操作失败') + } + } + + const columns = useMemo(()=>{ + return ((pageType === 'insideSystem' || pageStatus === 0 ) ? PUBLISH_APPROVAL_VERSION_INNER_TABLE_COLUMN:PUBLISH_APPROVAL_RECORD_INNER_TABLE_COLUMN).map(x=>{if(x.filters &&(x.dataIndex as string[])?.indexOf('creator') !== -1){x.valueEnum = memberValueEnum} return x}) + },[pageType, pageStatus, memberValueEnum]) + + useEffect(() => { + !init && pageListRef.current?.reload() + }, [pageStatus]); + + + useEffect(() => { + setPageStatus(Number(query.get('status') ||0) as 0|1) + }, [query]); + + useEffect(()=>{ + setPageType(currLocation.split('/')[0] === 'service' ? 'insideSystem' : 'global') + },[currLocation]) + + const manualReloadTable = () => { + setTableHttpReload(true); // 表格数据需要从后端接口获取 + pageListRef.current?.reload() + }; + + const drawerActions = { + approval: () => drawerRef.current?.save('pass'), + add: () => drawerRef.current?.publish(), + publish: () => drawerRef.current?.publish(true), + online: () => drawerRef.current?.online(), + }; + + + const onSubmit = () => { + const action = drawerActions[drawerType as keyof typeof drawerActions]; + if (action) { + return action()?.then((res) => { + if(drawerType === 'add' && res){ + handleOnline((res as unknown as Record)?.data?.publish) + } + if (res === true && (drawerType === 'online' || drawerType === 'add')) { + handleOnline(drawerData) + }else if(res === true){ + manualReloadTable(); + } + return res; + }); + } else { + return Promise.resolve(true); + } + }; + return ( + <> + getSystemPublishList(params)} + addNewBtnTitle={pageStatus === 0 ? "新建版本":''} + onAddNewBtnClick={()=>{openDrawer('add')}} + addNewBtnAccess="team.service.release.add" + onChange={() => { + setTableHttpReload(false) + }} + besidesTableHeight={58} + onRowClick={(row:PublishTableListItem|PublishVersionTableListItem)=>openDrawer('view',row)} + tableClickAccess="team.service.release.view" + /> + {setDrawerVisible(false)}} + open={drawerVisible} + okBtnTitle={drawerOkTitle} + submitDisabled={drawerType === 'add' ? !isOkToPublish : false} + submitAccess={`team.service.release.${drawerType === 'publish'? 'add' : drawerType}`} + cancelBtnTitle={drawerType === 'online' ? '关闭' : undefined} + showOkBtn={drawerType !== 'view'} + onSubmit={onSubmit} + extraBtn={(drawerType === 'approval'||drawerType === 'online') ? + + :undefined} + > + + + + ) +} +export default SystemInsidePublicList \ No newline at end of file diff --git a/frontend/packages/core/src/pages/system/publish/SystemInsidePublishOnline.tsx b/frontend/packages/core/src/pages/system/publish/SystemInsidePublishOnline.tsx new file mode 100644 index 00000000..edf50c03 --- /dev/null +++ b/frontend/packages/core/src/pages/system/publish/SystemInsidePublishOnline.tsx @@ -0,0 +1,69 @@ + +import { App, Table } from "antd"; +import { SYSTEM_PUBLISH_ONLINE_COLUMNS } from "../../../const/system/const"; +import { useEffect, useState } from "react"; +import { useFetch } from "@common/hooks/http"; +import { BasicResponse, STATUS_CODE } from "@common/const/const"; +import { EntityItem } from "@common/const/type"; + +type SystemInsidePublishOnlineProps = { + serviceId:string + teamId:string + id:string +} + +export type SystemInsidePublishOnlineItems = { + cluster:EntityItem + status:'done' | 'error' | 'publishing' + error:string +} +export default function SystemInsidePublishOnline(props:SystemInsidePublishOnlineProps ){ + const {serviceId, teamId, id} = props + const {message} = App.useApp() + const [dataSource, setDataSource] = useState<[]>() + const {fetchData} = useFetch() + const [isStopped, setIsStopped] = useState(false); + + const getOnlineStatus = ()=>{ + fetchData>('service/publish/status',{method:'GET',eoParams:{service:serviceId,team:teamId, id}, eoTransformKeys:['publish_status_list']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setDataSource(data.publishStatusList) + if(data.publishStatusList.filter((x:SystemInsidePublishOnlineItems)=>x.status === 'publishing').length === 0){ + setIsStopped(true) + } + }else{ + message.error(msg || '操作失败') + } + }).catch((errorInfo)=> message.error(errorInfo)) + } + + useEffect(()=>{ + getOnlineStatus(); + },[]) + + useEffect(() => { + let intervalId: NodeJS.Timeout; + if (!isStopped) { + intervalId = setInterval(() => { + !isStopped && getOnlineStatus(); + }, 5000); + } + + return () => { + clearInterval(intervalId); + }; + }, [isStopped]); + + return ( +
+ ) +} \ No newline at end of file diff --git a/frontend/packages/core/src/pages/system/upstream/SystemInsideUpstreamContent.tsx b/frontend/packages/core/src/pages/system/upstream/SystemInsideUpstreamContent.tsx new file mode 100644 index 00000000..958521df --- /dev/null +++ b/frontend/packages/core/src/pages/system/upstream/SystemInsideUpstreamContent.tsx @@ -0,0 +1,240 @@ + +import { App, Button, Divider, Form, Input, InputNumber, Radio, Select, Spin } from "antd"; +import {forwardRef, useEffect, useImperativeHandle, useRef, useState} from "react"; +import { LoadingOutlined } from "@ant-design/icons"; +import { GlobalNodeItem, ProxyHeaderItem, ServiceUpstreamFieldType, SystemInsideUpstreamConfigHandle, SystemInsideUpstreamContentHandle } from "../../../const/system/type.ts"; +import { FormItemProps } from "antd/es/form/index"; +import EditableTable from "@common/components/aoplatform/EditableTable.tsx"; +import EditableTableWithModal from "@common/components/aoplatform/EditableTableWithModal.tsx"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import { typeOptions, SYSTEM_UPSTREAM_GLOBAL_CONFIG_TABLE_COLUMNS, schemeOptions, balanceOptions, passHostOptions, PROXY_HEADER_CONFIG } from "../../../const/system/const.tsx"; +import { Link, useParams } from "react-router-dom"; +import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx"; +import { BasicResponse, STATUS_CODE } from "@common/const/const.ts"; +import { useFetch } from "@common/hooks/http.ts"; +import { useBreadcrumb } from "@common/contexts/BreadcrumbContext.tsx"; + +const DEFAULT_FORM_VALUE = { + driver:'static', + scheme:'HTTP', + balance:'round-robin', + limitPeerSecond:10000, + retry:3, + timeout:3, +} + +const SystemInsideUpstreamContent= forwardRef((props,ref) => { + const formRef = useRef(null) + const [loading, setLoading] = useState(false) + const { message } = App.useApp() + const { serviceId, teamId} = useParams(); + const {fetchData} = useFetch() + const [, forceUpdate] = useState(null); + const [formShowHost, setFormShowHost] = useState(false); + const { setBreadcrumb } = useBreadcrumb() + const [form] = Form.useForm(); + + useImperativeHandle(ref, () => ({ + save:()=>formRef.current?.save() + })); + + + const saveUpstream = ()=>{ + form.validateFields().then((value)=>{ + if(value.nodes){ + value.nodes = value.nodes.filter((x:GlobalNodeItem)=>x.address)?.map((x:GlobalNodeItem)=>({address:x.address, weight:x.weight ?? 100})) + } + value.limitPeerSecond = Number(value.limitPeerSecond)||0, + value.retry = Number(value.retry)||0, + value.timeout = Number(value.timeout)||0 + + return fetchData>( + 'service/upstream', + { + method:'PUT', + eoBody:({...value}), + eoParams:{service:serviceId,team:teamId}, + eoTransformKeys:['limitPeerSecond','proxyHeaders','optType','passHost','upstreamHost'] + }).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + return Promise.resolve(true) + }else{ + message.error(msg || '操作失败') + return Promise.reject(msg || '操作失败') + } + }).catch((errorInfo)=> {return Promise.reject(errorInfo)}) + }) + } + + // 获取表单默认值 + const getUpstreamInfo = () => { + setLoading(true) + fetchData>('service/upstream',{method:'GET',eoParams:{service:serviceId,team:teamId},eoTransformKeys:['limit_peer_second','proxy_headers','opt_type','global_config','pass_host','upstream_host']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setTimeout(()=>{ + form.setFieldsValue({...DEFAULT_FORM_VALUE,...data.upstream}) + setFormShowHost(data.upstream.passHost === 'rewrite') + },0) + }else{ + message.error(msg || '操作失败') + } + }).finally(()=>{ + setLoading(false) + forceUpdate({}) + }) + }; + + // 自定义校验规则 +const globalConfigNodesRule: FormItemProps['rules'] = [ + { + validator: (_, value) => { + if (!value || !Array.isArray(value)) { + return Promise.resolve(); + } + const filteredValue = value.filter((item) => item.address && item.weight!== '' && item.weight!== null); + if (filteredValue.length > 0) { + return Promise.resolve(); + } else { + return Promise.reject(new Error('必填项')); + } + }, + }, + ]; + + useEffect(() => { + setBreadcrumb([ + { + title: 内部数据服务 + }, + { + title: '上游' + }]) + + getUpstreamInfo(); + }, [serviceId]); + + return ( + } spinning={loading}> +
+ +
+ + + label="上游类型" + name="driver" + rules={[{ required: true, message: '必填项' }]} + > + + + + + + label="服务地址" + name="nodes" + tooltip="后端默认使用的IP地址" + rules={[{ required: true, message: '必填项' }, + ...globalConfigNodesRule]} + > + + configFields={SYSTEM_UPSTREAM_GLOBAL_CONFIG_TABLE_COLUMNS} + /> + + + + label="请求协议" + name="scheme" + rules={[{ required: true, message: '必填项' }]} + > + + + + + label="负载均衡" + name="balance" + rules={[{ required: true, message: '必填项' }]} + > + + + + + label="转发 Host" + name="passHost" + rules={[{ required: true, message: '必填项' }]} + > + + + + {formShowHost && + label="重写域名" + name="upstreamHost" + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + + } + + + + + label="超时时间" + name="timeout" + rules={[{ required: true, message: '必填项' }]} + > + ms }/> + + + + label="超时重试次数" + name="retry" + rules={[{ required: true, message: '必填项' }]} + > + 次} /> + + + + label="调用频率限制" + name="limitPeerSecond" + rules={[{ required: true, message: '必填项' }]} + > + 次/秒 } /> + + + + label="转发上游请求头" + name="proxyHeaders" + className="mb-0" + > + + configFields={PROXY_HEADER_CONFIG} + /> + + + + + + +
+
+
+ ) +}) + +export default SystemInsideUpstreamContent \ No newline at end of file diff --git a/frontend/packages/core/src/pages/team/Team.module.css b/frontend/packages/core/src/pages/team/Team.module.css new file mode 100644 index 00000000..98ff29a1 --- /dev/null +++ b/frontend/packages/core/src/pages/team/Team.module.css @@ -0,0 +1,17 @@ +.collapse-without-padding{ + :global .ant-collapse-expand-icon{ + padding-inline-start:0 !important; + padding-inline-end:12px !important; + } + :global .ant-collapse-content-box{ + padding:0px !important; + } + :global .ant-list-footer{ + padding:4px 0; + } + :global .ant-collapse-header { + background: #f7f8fa; + height: 40px; + border-radius: 4px; + } +} \ No newline at end of file diff --git a/frontend/packages/core/src/pages/team/TeamConfig.tsx b/frontend/packages/core/src/pages/team/TeamConfig.tsx new file mode 100644 index 00000000..d98c4595 --- /dev/null +++ b/frontend/packages/core/src/pages/team/TeamConfig.tsx @@ -0,0 +1,211 @@ +import { forwardRef, useEffect, useImperativeHandle, useState} from "react"; +import {App, Button, Divider, Form, Input, Row, Select} from "antd"; +import {Link, useLocation, useNavigate, useParams} from "react-router-dom"; +import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx"; +import { v4 as uuidv4 } from 'uuid' +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {MemberItem} from "@common/const/type.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import {DefaultOptionType} from "antd/es/cascader"; +import { TeamConfigFieldType } from "../../const/team/type.ts"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import { useBreadcrumb } from "@common/contexts/BreadcrumbContext.tsx"; +import { useTeamContext } from "../../contexts/TeamContext.tsx"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; + +export type TeamConfigHandle = { + save:()=>Promise|undefined +} + +type TeamConfigProps = { + entity?:TeamConfigFieldType +} + +const TeamConfig= forwardRef((props,ref) => { + const {entity} = props + const { message } = App.useApp() + const { teamId } = useParams(); + const [onEdit, setOnEdit] = useState(!!teamId) + const [form] = Form.useForm(); + const location = useLocation() + const currentUrl = location.pathname + const {fetchData} = useFetch() + const [managerOption, setManagerOption] = useState([]) + const { setBreadcrumb} = useBreadcrumb() + const { setTeamInfo } =useTeamContext() + const {checkPermission} = useGlobalContext() + const pageType= checkPermission('system.organization.team.view') ? 'manage' : 'myteam' + const [canDelete, setCanDelete] = useState(false) + const navigateTo = useNavigate() + useImperativeHandle(ref, () => ({ + save:onFinish + })); + + const onFinish = () => { + return form.validateFields().then((value)=>{ + let params:{[k:string]:string} = {} + if(pageType === 'manage'){ + params = {id:teamId!} + }else{ + params = {team:teamId!} + } + return fetchData>(pageType === 'manage'?'manager/team' : 'team',{method:onEdit ? 'PUT' : 'POST', eoParams:params,eoBody:(value),eoTransformKeys:['teamId']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + setTeamInfo?.(data.team) + return Promise.resolve(true) + }else{ + message.error(msg || '操作失败') + return Promise.reject(msg || '操作失败') + } + }).catch((errorInfo)=>{ + return Promise.reject(errorInfo) + }) + }) + }; + + // 获取表单默认值 + const getTeamInfo = () => { + fetchData>(pageType === 'manage'?'manager/team' : 'team',{method:'GET',eoParams:(pageType === 'manage'? {id:teamId}:{team:teamId}),eoTransformKeys:['can_delete']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setCanDelete(data.team.canDelete) + setTimeout(()=>{form.setFieldsValue({...data.team})},0) + }else{ + message.error(msg || '操作失败') + } + }) + }; + + const getManagerList = ()=>{ + setManagerOption([]) + fetchData>('simple/member',{method:'GET'}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setManagerOption(data.members?.map((x:MemberItem)=>{return { + label:x.name, value:x.id + }}) || []) + }else{ + message.error(msg || '操作失败') + } + }) + } + + const deleteTeam = ()=>{ + return new Promise((resolve, reject)=>{ + fetchData>(`manager/team`,{method:'DELETE',eoParams:{id:form.getFieldValue('id')}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + navigateTo('/team/list') + + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + useEffect(() => { + getManagerList() + if(entity){ + setOnEdit(true); + form.setFieldsValue(entity) + }else if (teamId !== undefined) { + setBreadcrumb([ + {title:团队}, + {title:'设置'} + ]) + setOnEdit(true); + getTeamInfo(); + } else { + setOnEdit(false); + form.setFieldsValue({id:uuidv4()}); // 清空 initialValues + } + // setPageType(currentUrl.split('/')[1] === 'myteam'? 'myteam':'manage') + return (form.setFieldsValue({})) + }, [teamId]); + + return ( + <> +
+ +
+ + label="团队名称" + name="name" + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + + + + label="团队 ID" + name="id" + extra="团队 ID(team_id)可用于检索团队,一旦保存无法修改。" + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + + + {!onEdit && + + label="团队负责人" + name="master" + extra="负责人对团队内的团队、服务、成员有管理权限" + rules={[{required: true, message: '必填项'}]} + > + + } + + + + label="描述" + name="description" + > + + + + { onEdit && + + + + } + {onEdit && + <> +
+

删除团队:删除操作不可恢复,请谨慎操作!

+
+ + + +
+
+ + } + +
+
+ + ) +}) +export default TeamConfig \ No newline at end of file diff --git a/frontend/packages/core/src/pages/team/TeamInsideMember.tsx b/frontend/packages/core/src/pages/team/TeamInsideMember.tsx new file mode 100644 index 00000000..8addf362 --- /dev/null +++ b/frontend/packages/core/src/pages/team/TeamInsideMember.tsx @@ -0,0 +1,336 @@ +import PageList from "@common/components/aoplatform/PageList.tsx" +import {ActionType, ProColumns} from "@ant-design/pro-components"; +import {FC, useEffect, useMemo, useRef, useState} from "react"; +import {Link, useParams} from "react-router-dom"; +import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import {App, Button, Modal, Select} from "antd"; +import {TransferTableHandle} from "@common/components/aoplatform/TransferTable.tsx"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx"; +import {EntityItem, MemberItem} from "@common/const/type.ts"; +import { TeamMemberTableListItem } from "../../const/team/type.ts"; +import { TEAM_MEMBER_TABLE_COLUMNS } from "../../const/team/const.tsx"; +import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx"; +import { checkAccess } from "@common/utils/permission.ts"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; +import MemberTransfer from "@common/components/aoplatform/MemberTransfer.tsx"; +import { DepartmentListItem } from "../../const/member/type.ts"; +import {v4 as uuidv4} from 'uuid' +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; + +export const getDepartmentWithMember = (department:(DepartmentListItem & {type?:'department'|'member'})[],departmentMap:Map) : (DepartmentWithMemberItem | undefined)[] =>{ + return department.map((x:DepartmentListItem & {type?:'department'|'member'})=>{ + const res = ({ + ...x, + key:x.id, + title:x.name, + type: x.type || 'department', + children:((x.type === 'member' || (!x.children||x.children.length === 0 )&& (!departmentMap.get(x.id) || departmentMap.get(x.id)!.length === 0))? undefined : [...(x.children && x.children.length > 0 ? getDepartmentWithMember(x.children,departmentMap) : []),...departmentMap.get(x.id) || []]) + }); + return res}).filter(node=>node.type === 'member' ||( node.children && node.children.length > 0)) +} + +export const addMemberToDepartment = (departmentMap: Map, departmentId: string, member: MemberItem) => { + const members = departmentMap.get(departmentId) || []; + members.push({...member, type: 'member'}); + departmentMap.set(departmentId, members); + } + +const TeamInsideMember:FC = ()=>{ + const [searchWord, setSearchWord] = useState('') + const { setBreadcrumb} = useBreadcrumb() + const { modal,message } = App.useApp() + const {fetchData} = useFetch() + const {teamId} = useParams(); + const addRef = useRef>(null) + const pageListRef = useRef(null); + const [allMemberIds, setAllMemberIds] = useState([]) + const {accessData} = useGlobalContext() + const [selectableMemberIds,setSelectableMemberIds] = useState>(new Set()) + const [addMemberBtnLoading, setAddMemberBtnLoading] = useState(false) + const [modalVisible, setModalVisible] = useState(false) + const [addMemberBtnDisabled, setAddMemberBtnDisabled] = useState(true) + const [allMemberSelectedDepartIds, setAllMemberSelectedDepartIds] = useState([]) + const [columns,setColumns] = useState[]>([]) + + const operation:ProColumns[] =[ + { + title: '操作', + key: 'option', + width: 76, + fixed:'right', + valueType: 'option', + render: (_: React.ReactNode, entity: TeamMemberTableListItem) => [ + {openModal('remove',entity)}} btnTitle="移出团队"/>] + } + ] + + const getDepartmentMemberList = () => { + const topDepartmentId:string = uuidv4() + return Promise.all([ + fetchData>('simple/departments', {method:'GET'}), + fetchData>('simple/member', {method:'GET', eoParams:{}, eoTransformKeys:[]}) + ]).then(([departmentResponse, memberResponse])=>{ + const departmentMap = new Map(); + memberResponse.data.members.forEach((member: MemberItem) => { + setSelectableMemberIds((pre)=>{pre.add(member.id);return pre}) + member = {...member, title:member.name, key:member.id} + if (member.department) { + member.department.forEach((department: EntityItem) => { + addMemberToDepartment(departmentMap, department.id, member); + }); + } else { + addMemberToDepartment(departmentMap, '_withoutDepartment', member); + } + }); + + const finalData = departmentResponse.data.department + ? [ + { + id: topDepartmentId, + key:topDepartmentId, + name: departmentResponse.data.department.name, + title:departmentResponse.data.department.name, + children: [ + ...getDepartmentWithMember(departmentResponse.data.department.children, departmentMap), + ...departmentMap.get('_withoutDepartment') || [] + ] + } + ] + : [...departmentMap.get('_withoutDepartment') || []]; + + + for(const [k,v] of departmentMap){ + if(k !== '_withoutDepartment' && allMemberIds.length > 0 ){ + // 筛选出部门内没被勾选的用户,如果不存在没勾选用户,需要将部门id放入ids中 + if(v.filter(m => allMemberIds.indexOf(m.id) === -1).length === 0){ + setAllMemberSelectedDepartIds((pre)=>[...pre, k]) + } + } + } + + if(!finalData[0].children || finalData[0].children.filter(m => allMemberIds.indexOf(m.id) === -1).length === 0){ + setAllMemberSelectedDepartIds((pre)=>[...pre, topDepartmentId]) + } + + return {data:finalData, success: true} + }).catch(()=>({data:[], success:false})) + } + + const getMemberList = ()=>{ + return fetchData>('team/members',{method:'GET',eoParams:{keyword:searchWord, team:teamId},eoTransformKeys:['attach_time','is_delete']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + if(!searchWord){ + setAllMemberIds(data.members?.map((x:TeamMemberTableListItem)=>x.user.id) || []) + } + return {data:data.members, success: true} + }else{ + message.error(msg || '操作失败') + return {data:[], success:false} + } + }).catch(() => { + return {data:[], success:false} + }) + } + + const addMember = (selectableMemberIds:Set)=>{ + setAddMemberBtnLoading(true) + const keyFromModal = addRef.current?.selectedRowKeys() + const memberKeyFromModal = keyFromModal?.filter(x => allMemberIds.indexOf(x as string) === -1 && selectableMemberIds.has(x)) || []; + return new Promise((resolve, reject)=>{ + fetchData>('team/member',{method:'POST' ,eoBody:({users:memberKeyFromModal}),eoParams:{team:teamId}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + manualReloadTable() + cleanModalData() + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)).finally(()=>setAddMemberBtnLoading(false)) + }) + } + + + const removeMember = (entity:TeamMemberTableListItem) =>{ + return new Promise((resolve, reject)=>{ + fetchData>(`team/member`,{method:'DELETE',eoParams:{team:teamId,user:entity.user.id}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + const cleanModalData = ()=>{ + setModalVisible(false);setAddMemberBtnDisabled(true);setAddMemberBtnLoading(false) + } + + const openModal = async (type:'add'|'remove',entity?:TeamMemberTableListItem)=>{ + let title:string = '' + let content:string|React.ReactNode = '' + switch(type){ + case 'add': + setModalVisible(true) + setAddMemberBtnDisabled(true) + setAddMemberBtnLoading(false) + return + case 'remove': + title='移除成员' + content=确定删除成员?此操作无法恢复,确认操作? + break + } + + modal.confirm({ + title, + content, + onOk:()=>{ + return removeMember(entity!).then((res)=>{if(res === true) manualReloadTable()}) + }, + width:600, + okText:'确认', + okButtonProps:{ + disabled: !checkAccess(`team.team.member.edit`,accessData) + }, + cancelText:'取消', + closable:true, + icon:<> + }) + } + + + const manualReloadTable = () => { + pageListRef.current?.reload() + }; + + + const changeMemberInfo = (value:string[],entity:TeamMemberTableListItem )=>{ + //console.log(value) + return new Promise((resolve, reject) => { + fetchData>(`team/member/role`, {method: 'PUT',eoBody:({roles:value, users:[entity.user.id]}), eoParams: {team:teamId}}).then(response => { + const {code, msg} = response + if (code === STATUS_CODE.SUCCESS) { + message.success(msg || '操作成功!') + resolve(true) + } else { + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + const getRoleList = ()=>{ + fetchData}>>('simple/roles', {method: 'GET', eoParams: {group:'team'}}).then(response => { + const {code, data,msg} = response + if (code === STATUS_CODE.SUCCESS) { + + const newCol = [...TEAM_MEMBER_TABLE_COLUMNS] + for(const col of newCol){ + //console.log(col) + if(col.dataIndex === 'roles'){ + col.render = (_,entity)=>( + + + + + + + + + + + + + + + + + + + + + + + + + {noticeType === 'single' && + + + + } + + + + + + + + + ) +}) + +export default function Webhook(){ + // const [searchWord, setSearchWord] = useState('') + // const navigate = useNavigate(); + const { setBreadcrumb } = useBreadcrumb() + // const [teamPageType,setTeamPageType]=useState( '') + const { modal,message } = App.useApp() + // const [confirmLoading, setConfirmLoading] = useState(false); + const {fetchData} = useFetch() + const [memberValueEnum, setMemberValueEnum] = useState<{[k:string]:{text:string}}>({}) + + const pageListRef = useRef(null); + const addRef = useRef(null) + const editRef = useRef(null) + const {accessData} = useGlobalContext() + + + const getWebhookList = ()=>{ + return fetchData>('webhooks',{method:'GET',eoTransformKeys:['content_type','create_time','is_delete','update_time']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + return {data:data.webhooks, success: true} + }else{ + message.error(msg || '操作失败') + return {data:[], success:false} + } + }).catch(() => { + return {data:[], success:false} + }) + } + + + const manualReloadTable = () => { + pageListRef.current?.reload() + }; + + + const deleteWebhook = (entity:WebhookTableListItem)=>{ + return new Promise((resolve, reject)=>{ + fetchData>('webhook',{method:'DELETE',eoParams:{uuid:entity!.uuid}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + const openModal = async (type:'add'|'edit'|'delete',entity?:WebhookTableListItem)=>{ + //console.log(type,entity) + let title:string = '' + let content:string | React.ReactNode= '' + switch (type){ + case 'add': + title='编辑 Webhook' + content= + break; + case 'edit':{ + title='编辑 Webhook' + message.loading('正在加载数据') + const {code,data,msg} = await fetchData>('webhook',{method:'GET',eoParams:{uuid:entity!.uuid},eoTransformKeys:['content_type','notice_type','user_separator']}) + message.destroy() + //console.log(data) + if(code === STATUS_CODE.SUCCESS){ + content= + }else{ + message.error(msg || '操作失败') + return + } + break;} + case 'delete': + title='删除' + content='该数据删除后将无法找回,请确认是否删除?' + break; + } + + modal.confirm({ + title, + content, + onOk:()=>{ + switch (type){ + case 'add': + return addRef.current?.save().then((res)=>{if(res === true) manualReloadTable()}) + case 'edit': + return editRef.current?.save().then((res)=>{if(res === true) manualReloadTable()}) + case 'delete': + return deleteWebhook(entity!).then((res)=>{if(res === true) manualReloadTable()}) + } + }, + width:900, + okText:'确认', + okButtonProps:{ + disabled : !checkAccess(`system.webhook.self.${type}` as (keyof typeof PERMISSION_DEFINITION[0]), accessData) + }, + cancelText:'取消', + closable:true, + icon:<>, + }) + } + + const operation:ProColumns[] =[ + { + title: '操作', + key: 'option', + width: 93, + valueType: 'option', + render: (_: React.ReactNode, entity: WebhookTableListItem) => + [{openModal('edit',entity)}} btnTitle="查看"/>, + , + {openModal('delete',entity)}} btnTitle="删除"/>] + } + ] + + + const getMemberList = async ()=>{ + setMemberValueEnum({}) + const {code,data,msg} = await fetchData>('simple/member',{method:'GET'}) + if(code === STATUS_CODE.SUCCESS){ + const tmpValueEnum:{[k:string]:{text:string}} = {} + data.members?.forEach((x:SimpleMemberItem)=>{ + tmpValueEnum[x.name] = {text:x.name} + }) + setMemberValueEnum(tmpValueEnum) + }else{ + message.error(msg || '操作失败') + } + } + + useEffect(() => { + setBreadcrumb([ + {title:'Webhook 管理'} + ]) + getMemberList() + }, []); + + + const columns = useMemo(()=>{ + return WEBHOOK_TABLE_COLUMNS.map(x=>{if(x.filters &&((x.dataIndex as string[])?.indexOf('updater') !== -1) ){x.valueEnum = memberValueEnum} return x}) + },[memberValueEnum]) + + return ( + getWebhookList()} + primaryKey="uuid" + showPagination={false} + addNewBtnTitle="添加 Webhook" + onAddNewBtnClick={()=>{openModal('add')}} + onRowClick={(row:WebhookTableListItem)=>openModal('edit',row)} + /> + ) + +} \ No newline at end of file diff --git a/frontend/packages/core/src/vite-env.d.ts b/frontend/packages/core/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/frontend/packages/core/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/packages/core/start-vite.js b/frontend/packages/core/start-vite.js new file mode 100644 index 00000000..f55d1cb4 --- /dev/null +++ b/frontend/packages/core/start-vite.js @@ -0,0 +1,22 @@ +/* + * @Date: 2024-06-05 09:35:25 + * @LastEditors: maggieyyy + * @LastEditTime: 2024-06-05 10:50:12 + * @FilePath: \frontend\packages\core\start-vite.js + */ +// start-vite.js// start-vite.js +import { exec } from 'child_process'; + +const viteProcess = exec('pnpm run build'); + +viteProcess.stdout.on('data', (data) => { + console.log(data.toString()); +}); + +viteProcess.stderr.on('data', (data) => { + console.error(data.toString()); +}); + +viteProcess.on('close', (code) => { + console.log(`Vite process exited with code ${code}`); +}); diff --git a/frontend/packages/core/tailwind.config.js b/frontend/packages/core/tailwind.config.js new file mode 100644 index 00000000..c98c8b02 --- /dev/null +++ b/frontend/packages/core/tailwind.config.js @@ -0,0 +1,87 @@ +/* + * @Date: 2023-11-27 17:31:44 + * @LastEditors: maggieyyy + * @LastEditTime: 2024-06-05 10:36:46 + * @FilePath: \frontend\packages\core\tailwind.config.js + */ +/** @type {import('tailwindcss').Config} */ + +export default { + important:true, + content: [ + `./index.html`, + `../*/src/**/*.{js,ts,jsx,tsx}`, + + ], + theme: { + extend: { + width: { + INPUT_NORMAL: '100%', + // INPUT_NORMAL: '346px', + INPUT_LARGE: '508px', + GROUP: '240px', + SEARCH: '276px', + LOG: '254px' + }, + minHeight:{ + TEXTAREA:'68px' + }, + borderRadius: { + DEFAULT: 'var(--border-radius)', + SEARCH_RADIUS: '50px' + }, + boxShadow:{ + SCROLL: '0 2px 2px #0000000d', + SCROLL_TOP:' 0 -2px 2px -2px var(--border-color)' + }, + colors: { + DISABLE_BG: 'var(--disabled-background-color)', + MAIN_TEXT: 'var(--text-color)', + MAIN_HOVER_TEXT: 'var(--text-hover-color)', + SECOND_TEXT:'var(--disabled-text-color)', + MAIN_BG: 'var(--background-color)', + MENU_BG:'var(--MENU-BG-COLOR)', + 'bar-theme': 'var(--bar-background-color)', + BORDER: 'var(--border-color)', + NAVBAR_BTN_BG: 'var(--item-active-background-color)', + MAIN_DISABLED_BG: 'var(--disabled-background-color)', + theme: 'var(--primary-color)', + DESC_TEXT: 'var(--TITLE_TEXT)', + HOVER_BG: 'var(--item-hover-background-color)', + guide_cluster: '#ee6760', + guide_upstream: '#f9a429', + guide_api: '#71d24d', + guide_publishApi: '#5884ff', + guide_final: '#915bf9', + table_text: 'var(--table-text-color)', + status_success:'#138913', + status_fail:"#ff3b30", + status_update:"#03a9f4", + status_pending:"#ffa500", + status_offline:"#8f8e93", + A_HOVER:'var(--button-primary-hover-background-color)' + }, + spacing: { + mbase: 'var(--FORM_SPAN)', + label: '12px', // 选择器和label之间的间距,待删 + btnbase: 'var(--LAYOUT_MARGIN)', // x方向的间距 + btnybase: 'var(--LAYOUT_MARGIN)', // y轴方向的间距 + btnrbase: '20px', // 页面最右侧边距20px + formtop: 'var(--FORM_SPAN)', + icon: '5px', + blockbase: '40px', + DEFAULT_BORDER_RADIUS: 'var(--border-radius)', + TREE_TITLE:'var(--small-padding) var(--LAYOUT_PADDING);', + }, + borderColor: { + 'color-base': 'var(--border-color)' + } + } + }, + plugins: [], + corePlugins: { + preflight: false, + }, + } + + \ No newline at end of file diff --git a/frontend/packages/core/tsconfig.json b/frontend/packages/core/tsconfig.json new file mode 100644 index 00000000..fc1be9b1 --- /dev/null +++ b/frontend/packages/core/tsconfig.json @@ -0,0 +1,30 @@ + +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + /* Linting */ + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "paths": { + "@core/*": ["./src/*"], + "@common/*": ["../common/src/*"], + "@market/*": ["../market/src/*"], + }, + }, + "include": ["src", "public/iconpark_eolink.js", "public/iconpark_apinto.js", "../common/src/component/aoplatform/EditableTableWithModal.tsx", "../common/src/components/aoplatform/TransferTable.tsx", "../common/src/components/aoplatform/TreeWithMore.tsx", "../common/src/components/aoplatform/DatePicker.tsx", "../common/src/components/aoplatform/TimeRangeSelector.tsx", "../common/src/components/aoplatform/TimePicker.tsx", "../common/src/components/aoplatform/MemberTransfer.tsx", "../common/src/components/aoplatform/Navigation.tsx", "../common/src/components/aoplatform/PageList.tsx", "../common/src/components/aoplatform/GroupTree.tsx", "../common/src/components/aoplatform/ErrorBoundary.tsx", "../common/src/components/aoplatform/ScrollableSection.tsx", "../common/src/utils/postcat.tsx", "../common/src/utils/curl.ts", "../common/src/components/aoplatform/ResetPsw.tsx", "../common/src/components/aoplatform/SubscribeApprovalModalContent.tsx", "../common/src/components/aoplatform/InsidePageForHub.tsx", "src/components/aoplatform/RenderRoutes.tsx", "../common/src/components/aoplatform/PublishApprovalModalContent.tsx", "../common/src/components/aoplatform/InsidePage.tsx", "../common/src/const/type.ts", "../common/src/components/aoplatform/intelligent-plugin", "../common/src/const/domain"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/packages/core/tsconfig.node.json b/frontend/packages/core/tsconfig.node.json new file mode 100644 index 00000000..42872c59 --- /dev/null +++ b/frontend/packages/core/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/packages/core/vite.config.ts b/frontend/packages/core/vite.config.ts new file mode 100644 index 00000000..10964f3f --- /dev/null +++ b/frontend/packages/core/vite.config.ts @@ -0,0 +1,77 @@ + +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' +import dynamicImportVars from '@rollup/plugin-dynamic-import-vars'; +import tailwindcss from 'tailwindcss'; +import autoprefixer from 'autoprefixer'; + +export default defineConfig({ + cacheDir: './node_modules/.vite', + build:{ + outDir:'../../dist', + sourcemap: false, + chunkSizeWarningLimit: 50000, + cacheDir: './node_modules/.vite', + output: { + manualChunks(id) { + if (id.includes('node_modules')) { + return id.toString().split('node_modules/')[1].split('/')[0].toString(); + } + // 针对 pnpm 和 Monorepo 特殊处理 + if (id.includes('.pnpm')) { + const segments = id.split(path.sep); + const packageName = segments[segments.indexOf('.pnpm') + 1].split('@')[0]; + return packageName; + } + } + }, + }, + css: { + postcss: { + plugins: [ + tailwindcss(path.resolve(__dirname, '../common/tailwind.config.js')), + autoprefixer + ], + }, + preprocessorOptions: { + less: { + javascriptEnabled: true, + }, + }, + modules:{ + localsConvention:"camelCase", + generateScopedName:"[local]_[hash:base64:2]" + } + }, + plugins: [react(), + dynamicImportVars({ + include:["src"], + exclude:[], + warnOnError:false + }), + ], + resolve: { + alias: [ + { find: /^~/, replacement: '' }, + { find: '@common', replacement: path.resolve(__dirname, '../common/src') }, + { find: '@market', replacement: path.resolve(__dirname, '../market/src') }, + { find: '@core', replacement: path.resolve(__dirname, './src') }, + ] + }, + server: { + proxy: { + '/api/v1': { + // target: 'http://uat.apikit.com:11204/mockApi/aoplatform/', + target: 'http://172.18.166.219:8288/', + changeOrigin: true, + }, + '/api2/v1': { + // target: 'http://uat.apikit.com:11204/mockApi/aoplatform/', + target: 'http://172.18.166.219:8288/', + changeOrigin: true, + } + } + }, + logLevel:'info' +}) diff --git a/frontend/packages/dashboard/README.md b/frontend/packages/dashboard/README.md new file mode 100644 index 00000000..12dcb526 --- /dev/null +++ b/frontend/packages/dashboard/README.md @@ -0,0 +1,11 @@ +# `dashboard` + +> TODO: description + +## Usage + +``` +const dashboard = require('dashboard'); + +// TODO: DEMONSTRATE API +``` diff --git a/frontend/packages/dashboard/__tests__/dashboard.test.js b/frontend/packages/dashboard/__tests__/dashboard.test.js new file mode 100644 index 00000000..875278cb --- /dev/null +++ b/frontend/packages/dashboard/__tests__/dashboard.test.js @@ -0,0 +1,7 @@ +'use strict'; + +const dashboard = require('..'); +const assert = require('assert').strict; + +assert.strictEqual(dashboard(), 'Hello from dashboard'); +console.info('dashboard tests passed'); diff --git a/frontend/packages/dashboard/package.json b/frontend/packages/dashboard/package.json new file mode 100644 index 00000000..bb61312c --- /dev/null +++ b/frontend/packages/dashboard/package.json @@ -0,0 +1,11 @@ +{ + "name": "dashboard", + "version": "0.0.0", + "description": "dashboard for AO Platform", + "author": "maggieyyy ", + "homepage": "", + "license": "ISC", + "dependencies": { + "echarts-for-react": "^3.0.2" + } +} diff --git a/frontend/packages/dashboard/postcss.config.js b/frontend/packages/dashboard/postcss.config.js new file mode 100644 index 00000000..80393090 --- /dev/null +++ b/frontend/packages/dashboard/postcss.config.js @@ -0,0 +1,15 @@ +/* + * @Date: 2023-11-27 17:31:54 + * @LastEditors: maggieyyy + * @LastEditTime: 2023-11-29 15:49:05 + * @FilePath: \applatform\frontend\packages\core\postcss.config.js + */ +export default { + plugins: { + 'postcss-import': {}, + 'tailwindcss/nesting': {}, + tailwindcss: {}, + autoprefixer: {} + }, + } + \ No newline at end of file diff --git a/frontend/packages/dashboard/src/component/MonitorApiPage.tsx b/frontend/packages/dashboard/src/component/MonitorApiPage.tsx new file mode 100644 index 00000000..e9115635 --- /dev/null +++ b/frontend/packages/dashboard/src/component/MonitorApiPage.tsx @@ -0,0 +1,227 @@ +import { CloseOutlined, ExpandOutlined, SearchOutlined } from "@ant-design/icons"; +import { Select, Input, Button, App, Drawer } from "antd"; +import { debounce } from "lodash-es"; +import { useState, useEffect, useRef } from "react"; +import { MonitorApiData, SearchBody } from "@dashboard/const/type"; +import { getTime } from "../utils/dashboard"; +import ScrollableSection from "@common/components/aoplatform/ScrollableSection"; +import TimeRangeSelector, { RangeValue, TimeRange, TimeRangeButton } from "@common/components/aoplatform/TimeRangeSelector"; +import MonitorTable, { MonitorTableHandler } from "./MonitorTable"; +import { BasicResponse, STATUS_CODE } from "@common/const/const"; +import { DefaultOptionType } from "antd/es/select"; +import { useParams } from "react-router-dom"; +import { RouterParams } from "@core/components/aoplatform/RenderRoutes"; +import { useExcelExport } from "@common/hooks/excel"; +import { API_TABLE_GLOBAL_COLUMNS_CONFIG } from "@dashboard/const/const"; +import { useFetch } from "@common/hooks/http"; +import { EntityItem } from "@common/const/type"; +export type MonitorApiPageProps = { + fetchTableData:(body:SearchBody)=>Promise> + detailDrawerContent:React.ReactNode + fullScreen?:boolean + setFullScreen?:(val:boolean) => void + setDetailId:(val:string) =>void + setTimeButton:(val:TimeRangeButton) => void + timeButton:TimeRangeButton + setDetailEntityName:(name:string) => void + detailEntityName:string +} + +export type MonitorApiQueryData = SearchBody & { path?:string, apis?:string[], projects?:string[] } + +export default function MonitorApiPage(props:MonitorApiPageProps){ + const {fetchTableData,detailDrawerContent,fullScreen,setFullScreen,setDetailId,timeButton,setTimeButton,detailEntityName,setDetailEntityName} = props + const {message} = App.useApp() + const [datePickerValue, setDatePickerValue] = useState(); + const [queryData, setQueryData] = useState(); + const [exportLoading, setExportLoading] = useState(false); + const monitorApiTableRef = useRef(null) + const {exportExcel} = useExcelExport() + const [drawerOpen, setDrawerOpen] = useState(false); + const [apiOptionList, setApiOptionList] = useState([]) + const [projectOptionList, setProjectOptionList] = useState([]) + const [queryBtnLoading, setQueryBtnLoading] = useState(false) + const {fetchData} = useFetch() + + useEffect(() => { + getMonitorData(); + getApiList(); + getProjectList(); + }, []); + + const getApiList = (projectIds?:string[])=>{ + return fetchData<{apis:EntityItem[]}>('simple/project/apis',{method:'POST',eoBody:({projects:projectIds || queryData?.projects})}).then((resp) => { + const {code,data,msg} = resp + if(code === STATUS_CODE.SUCCESS){ + setApiOptionList(data.apis?.map((x:EntityItem)=>({label:x.name, value:x.id}))) + }else{ + message.error(msg || '获取数据失败,请重试') + return setApiOptionList([]) + } + }).catch(() => { + return setApiOptionList([]) + }) + } + + const getProjectList = ()=>{ + return fetchData<{projects:EntityItem[]}>('simple/projects',{method:'GET'}).then((resp) => { + const {code,data,msg} = resp + if(code === STATUS_CODE.SUCCESS){ + setProjectOptionList(data.projects?.map((x:EntityItem)=>({label:x.name, value:x.id}))) + }else{ + message.error(msg || '获取数据失败,请重试') + return setProjectOptionList([]) + } + }).catch(() => { + return setProjectOptionList([]) + }) + } + + const getMonitorData = () => { + let query = queryData + if(!queryData || queryData.start === undefined){ + const { startTime, endTime } = getTime(timeButton, datePickerValue||[],) + query={...query,start: startTime, end: endTime } + } + const data:SearchBody = query! + setQueryData(data) + }; + + const getApiTableList = () => { + // ...根据时间和集群获取监控数据... + let query = queryData + if(!queryData || queryData.start === undefined){ + const { startTime, endTime } = getTime(timeButton, datePickerValue||[],) + query={...query,start: startTime, end: endTime } + } + const data:SearchBody = query! + setQueryData(data) + monitorApiTableRef.current?.reload() + }; + + const exportData = () => { + setExportLoading(true); + let query = queryData + if(!queryData || queryData.start === undefined){ + const { startTime, endTime } = getTime(timeButton, datePickerValue||[],) + query={...query,start: startTime, end: endTime } + } + const data:SearchBody = query! ; + fetchTableData(data).then((resp) => { + const {code,data,msg} = resp + if(code === STATUS_CODE.SUCCESS){ + exportExcel('API调用统计', [query!.start!, query!.end!], 'API调用统计', 'dashboard_api', API_TABLE_GLOBAL_COLUMNS_CONFIG, data.statistics) + }else{ + message.error(msg || '获取数据失败,请重试') + } + }) + }; + + const clearSearch = () => { + setTimeButton('hour'); + setDatePickerValue(null) + setQueryData(undefined); + }; + + const handleTimeRangeChange = (timeRange:TimeRange) => { + setQueryData(pre => ({...pre, ...timeRange} as SearchBody )) + }; + + const getTablesData = (body: SearchBody) => { + return fetchTableData(body).then((resp) => { + const {code,data,msg} = resp + setQueryBtnLoading(false) + if(code === STATUS_CODE.SUCCESS){ + return {data:data.statistics?.map((x:MonitorApiData)=>{x.proxyRate = Number((x.proxyRate*100).toFixed(2));x.requestRate = Number((x.requestRate*100).toFixed(2));return x}), success: true} + }else{ + message.error(msg || '获取数据失败,请重试') + return {data:[], success:false} + } + }).catch(() => { + setQueryBtnLoading(false) + return {data:[], success:false} + }) + }; + + const getDetailData = (entity:MonitorApiData)=>{ + setDetailEntityName(entity.name) + setDetailId(entity.id) + setDrawerOpen(true) + } + + return ( +
+ +
+ +
+ + {setQueryData(prevData=>({...prevData || {}, apis:value}))}} + /> + +
+ {/* setQueryData({ ...queryData, path: '' })} /> */} + debounce((e)=>{setQueryData(prevData=>({...prevData || {}, path:e.target.value}))}, 100)(e)} allowClear placeholder='请输入请求路径进行搜索' prefix={}/> +
+ + + +
+
+
+ {getDetailData(record); }} request={()=>getTablesData(queryData||{})} showPagination={true}/> +
+
+ + + {fullScreen && {setFullScreen?.(false)}}> + 退出全屏 + } + {detailEntityName}调用详情 + {!fullScreen && {setFullScreen?.(true)}}/>} + } + width={fullScreen ? '100%' : '60%'} + onClose={()=>setDrawerOpen(false)} + open={drawerOpen}> + {detailDrawerContent} + +
+ ) +} \ No newline at end of file diff --git a/frontend/packages/dashboard/src/component/MonitorAppPage.tsx b/frontend/packages/dashboard/src/component/MonitorAppPage.tsx new file mode 100644 index 00000000..b804e7c9 --- /dev/null +++ b/frontend/packages/dashboard/src/component/MonitorAppPage.tsx @@ -0,0 +1,192 @@ +import { Select, Button, App, Drawer } from "antd"; +import { useEffect, useRef, useState } from "react"; +import { MonitorSubscriberData, SearchBody } from "@dashboard/const/type"; +import { EntityItem } from "@common/const/type"; +import TimeRangeSelector, { RangeValue, TimeRange, TimeRangeButton } from "@common/components/aoplatform/TimeRangeSelector"; +import MonitorTable, { MonitorTableHandler } from "./MonitorTable"; +import { DefaultOptionType } from "antd/es/select"; +import { BasicResponse, STATUS_CODE } from "@common/const/const"; +import { getTime } from "../utils/dashboard"; +import { useExcelExport } from "@common/hooks/excel"; +import { APPLICATION_TABLE_GLOBAL_COLUMNS_CONFIG } from "@dashboard/const/const"; +import { CloseOutlined, ExpandOutlined } from "@ant-design/icons"; +import { useFetch } from "@common/hooks/http"; +import { MonitorSubQueryData } from "./MonitorSubPage"; + +export type MonitorAppPageProps = { + fetchTableData:(body:SearchBody)=>Promise> + fetchAppListData?:(body:SearchBody)=>Promise> + detailDrawerContent:React.ReactNode + fullScreen?:boolean + setFullScreen?:(val:boolean) => void + setDetailId:(val:string) =>void + setTimeButton:(val:TimeRangeButton) => void + timeButton:TimeRangeButton + setDetailEntityName:(name:string) => void + detailEntityName:string +} + +export default function MonitorAppPage(props:MonitorAppPageProps){ + const {fetchTableData,detailDrawerContent,fullScreen,setFullScreen,setDetailId,timeButton,setTimeButton,detailEntityName,setDetailEntityName} = props + const {message} = App.useApp() + const [queryData, setQueryData] = useState({type:'subscriber'}); + const [exportLoading, setExportLoading] = useState(false); + const [datePickerValue, setDatePickerValue] = useState(); + const monitorAppTableRef = useRef(null) + const {exportExcel} = useExcelExport() + const [drawerOpen, setDrawerOpen] = useState(false); + const [listOfApps, setListOfApps] = useState([]) + const {fetchData} = useFetch() + const [queryBtnLoading, setQueryBtnLoading] = useState(false) + + useEffect(() => { + getMonitorData(); + getAppList() + }, []); + + const getMonitorData = () => { + let query = queryData + if(!queryData || queryData.start === undefined){ + const { startTime, endTime } = getTime(timeButton, datePickerValue||[],) + query={...query,start: startTime, end: endTime } + } + const data:SearchBody = query! + setQueryData(data) + }; + + const getAppList = ()=>{ + return fetchData<{projects:EntityItem[]}>('simple/apps/mine',{method:'GET'}).then((resp) => { + const {code,data,msg} = resp + if(code === STATUS_CODE.SUCCESS){ + setListOfApps(data.projects?.map((x:EntityItem)=>({label:x.name, value:x.id}))) + }else{ + message.error(msg || '获取数据失败,请重试') + return setListOfApps([]) + } + }).catch(() => { + return setListOfApps([]) + }) + } + + const clearSearch = () => { + setTimeButton('hour'); + setDatePickerValue(null) + setQueryData({type:'subscriber'}); + } + + const getAppTableList = () => { + // ...根据时间和集群获取监控数据... + let query = queryData + if(!queryData || queryData.start === undefined){ + const { startTime, endTime } = getTime(timeButton, datePickerValue||[],) + query={...query,start: startTime, end: endTime } + } + const data:SearchBody = query! + setQueryData(data) + monitorAppTableRef.current?.reload() + }; + + + const exportData = () => { + setExportLoading(true); + let query = queryData + if(!queryData || queryData.start === undefined){ + const { startTime, endTime } = getTime(timeButton, datePickerValue||[],) + query={...query,start: startTime, end: endTime } + } + const data:SearchBody = query! ; + fetchTableData(data).then((resp) => { + const {code,data,msg} = resp + if(code === STATUS_CODE.SUCCESS){ + exportExcel('应用调用统计', [query!.start!, query!.end!], '应用调用统计', 'dashboard_application', APPLICATION_TABLE_GLOBAL_COLUMNS_CONFIG, data.statistics) + }else{ + message.error(msg || '获取数据失败,请重试') + } + }) + }; + + const handleTimeRangeChange = (timeRange:TimeRange) => { + setQueryData(pre => ({...pre, ...timeRange} as SearchBody )) + }; + + + const getTablesData = (body: SearchBody) => { + return fetchTableData(body).then((resp) => { + const {code,data,msg} = resp + setQueryBtnLoading(false) + if(code === STATUS_CODE.SUCCESS){ + return {data:data.statistics?.map((x:MonitorSubscriberData)=>{x.proxyRate = Number((x.proxyRate*100).toFixed(2));x.requestRate = Number((x.requestRate*100).toFixed(2));return x}), success: true} + }else{ + message.error(msg || '获取数据失败,请重试') + return {data:[], success:false} + } + }).catch(() => { + setQueryBtnLoading(false) + return {data:[], success:false} + }) + }; + + + const getDetailData = (entity:MonitorSubscriberData)=>{ + setDetailEntityName(entity.name) + setDetailId(entity.id) + setDrawerOpen(true) + } + + return ( +
+
+ +
+
+ + `and ${selectedList.length} more selected`} + placeholder="请选择" + value={queryData?.projects} + options={listOfProjects} + onChange={(value)=>{setQueryData(prevData=>({...prevData || {}, projects:value}))}} + /> +
+
+ + + +
+
+
+
+ {getDetailData(record); }} request={()=>getTablesData(queryData||{})} showPagination={true}/> +
+ + + {fullScreen && {setFullScreen?.(false)}}> + 退出全屏 + } + {detailEntityName}调用详情 + {!fullScreen && {setFullScreen?.(true)}}/>} + } + width={fullScreen ? '100%' : '60%'} + onClose={()=>setDrawerOpen(false)} + open={drawerOpen}> + {detailDrawerContent} + +
) +} \ No newline at end of file diff --git a/frontend/packages/dashboard/src/component/MonitorTable.tsx b/frontend/packages/dashboard/src/component/MonitorTable.tsx new file mode 100644 index 00000000..30ede380 --- /dev/null +++ b/frontend/packages/dashboard/src/component/MonitorTable.tsx @@ -0,0 +1,103 @@ + +import { ActionType, ProColumns } from "@ant-design/pro-components" +import { useImperativeHandle, useRef, useState } from "react" +import PageList from "@common/components/aoplatform/PageList" +import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission" +import { API_TABLE_GLOBAL_COLUMNS_CONFIG,SERVICE_TABLE_GLOBAL_COLUMNS_CONFIG, APPLICATION_TABLE_GLOBAL_COLUMNS_CONFIG } from "@dashboard/const/const" +import {forwardRef} from "react" + +const TableType = { + api :API_TABLE_GLOBAL_COLUMNS_CONFIG, + provider :SERVICE_TABLE_GLOBAL_COLUMNS_CONFIG, + subscribers :APPLICATION_TABLE_GLOBAL_COLUMNS_CONFIG +} + +type MonitorTableProps = { + type:'api'|'subscribers'|'provider' + id:string + request:(keyword?:string) => Promise<{ + data: T[]; + success: boolean; + }> + onRowClick:(record:T)=>void + searchPlaceholder?:string + showPagination?:boolean + noTop?:boolean + minVirtualHeight?:number + className?:string + inModal?:boolean +} + +export interface MonitorTableHandler{ + reload:()=>void +} + +const MonitorTable = forwardRef>((props,ref) => { + const {type,id,request,onRowClick,searchPlaceholder,showPagination=false,noTop,minVirtualHeight,className,inModal=false} = props + const [searchWord, setSearchWord] = useState('') + const tableRef = useRef(null) + const [tableHttpReload, setTableHttpReload] = useState(true); + const [tableListDataSource, setTableListDataSource] = useState([]); + + useImperativeHandle(ref,()=>({ + reload: ()=>{tableRef.current?.reload()} + })) + + const getTableDataSource = ()=>{ + if(!tableHttpReload){ + setTableHttpReload(true) + return Promise.resolve({ + data: tableListDataSource, + success: true, + }); + } + return request(searchWord).then(response=>{ + const {data,success} = response + setTableListDataSource(data) + return {data, success} + }).catch(() => { + return {data:[], success:false} + }) + } + + const operation:ProColumns[] =[ + { + title: '操作', + key: 'option', + width: 98, + fixed:'right', + hideInSetting:true, + valueType: 'option', + render: (_: React.ReactNode, entity: unknown) => [ + // onRowClick(entity)} btnTitle="查看"/>, + onRowClick(entity)} btnTitle="查看"/>, + ], + } + ] + + return ( +
+ { + setSearchWord(e.target.value) + }} + onChange={() => { + setTableHttpReload(false) + }} + noTop={noTop} + />
) +}) + +export default MonitorTable; \ No newline at end of file diff --git a/frontend/packages/dashboard/src/component/MonitorTotalPage.tsx b/frontend/packages/dashboard/src/component/MonitorTotalPage.tsx new file mode 100644 index 00000000..e63a9d3e --- /dev/null +++ b/frontend/packages/dashboard/src/component/MonitorTotalPage.tsx @@ -0,0 +1,335 @@ + +import { App, Select, Button, Tabs, TabsProps, Empty, Drawer, Spin } from "antd"; +import dayjs from "dayjs"; +import customParseFormat from "dayjs/plugin/customParseFormat"; +import { useState, useEffect, useRef, useReducer } from "react"; +import { useParams } from "react-router-dom"; +import { BasicResponse, STATUS_CODE } from "@common/const/const"; +import { SummaryPieData, SearchBody, PieData, MonitorApiData, MonitorSubscriberData, InvokeData, MessageData } from "@dashboard/const/type"; +import { getTime, getTimeUnit, changeNumberUnit } from "../utils/dashboard"; +import { RouterParams } from "@core/components/aoplatform/RenderRoutes"; +import ScrollableSection from "@common/components/aoplatform/ScrollableSection"; +import { RangeValue, TimeRange } from "@common/components/aoplatform/TimeRangeSelector"; +import TimeRangeSelector from "@common/components/aoplatform/TimeRangeSelector"; +import MonitorLineGraph from "./MonitorLineGraph"; +import MonitorPieGraph from "./MonitorPieGraph"; +import MonitorTable, { MonitorTableHandler } from "./MonitorTable"; +import { CloseOutlined, ExpandOutlined, LoadingOutlined } from "@ant-design/icons"; +import DashboardDetail from "@dashboard/pages/DashboardDetail"; + +dayjs.extend(customParseFormat); + +export type MonitorTotalPageProps = { + fetchPieData:(body:SearchBody)=>Promise> + fetchInvokeData:(body:SearchBody)=>Promise> + fetchMessageData:(body:SearchBody)=>Promise> + fetchTableData:(body:SearchBody,type: 'api' | 'subscriber'|'provider')=>Promise> + goToDetail:(body:SearchBody,val: MonitorApiData|MonitorSubscriberData, type: string) => void +} + +const ACTIONS = { + REQUEST_COMPLETE: 'REQUEST_COMPLETE', + RESET: 'RESET', +}; + +const initialState = { + getPieData: false, + getInvokeData: false, + getMessageData: false, + getTablesData: false, +}; + +function reducer(state: typeof initialState, action: { type: string, payload?: string }) { + switch (action.type) { + case ACTIONS.REQUEST_COMPLETE: + return { ...state, [action.payload!]: true }; + case ACTIONS.RESET: + return initialState; + default: + return state; + } +} + +const MonitorTotalPage = (props:MonitorTotalPageProps) => { + const {fetchPieData,fetchInvokeData,fetchMessageData,fetchTableData} = props + const { message } = App.useApp() + const [ queryData, setQueryData] = useState() + const [timeButton, setTimeButton] = useState<''|'hour'|'day'|'threeDays'|'sevenDays'>('hour'); + const [datePickerValue, setDatePickerValue] = useState(); + const [requestStatic, setRequestStatic] = useState(); + const [proxyStatic, setProxyStatic] = useState(); + const [requestPie, setRequestPie] = useState<{ [key: string]: number }>({}); + const [proxyPie, setProxyPie] = useState<{ [key: string]: number }>({}); + const [requestSucRate, setRequestSucRate] = useState('0%'); + const [proxySucRate, setProxySucRate] = useState('0%'); + const [invokeStatic, setInvokeStatic] = useState({ date: [], requestRate: [], requestTotal: [], proxyRate: [], proxyTotal: [], status_4xx: [], status_5xx: [] }); + const [trafficStatic, setTrafficStatic] = useState({ date: [], requestMessage: [], responseMessage: [] }); + const [pieError, setPieError] = useState(false) + const [invokeStaticError,setInvokeStaticError] = useState(false) + const [trafficStaticError,setTrafficStaticError] = useState(false) + const [timeUnit, setTimeUnit] = useState() + const monitorApiTableRef = useRef(null) + const monitorSubTableRef = useRef(null) + const [detailEntityName,setDetailEntityName]= useState('') + const [detailType,setDetailType]= useState<'api'|'provider'|'subscriber'>() + const [drawerOpen, setDrawerOpen] = useState(false); + const [detailId, setDetailId] = useState() + const [fullScreen, setFullScreen] = useState(false) + const [recordQuery, setRecordQuery] = useState() + const [queryBtnLoading, setQueryBtnLoading] = useState(false) + const [totalEmpty, setTotalEmpty] = useState(true) + const [requestStatus, dispatch] = useReducer(reducer, initialState); + + useEffect(() => { + const isLoading = Object.values(requestStatus).every(status => status !== true); + setQueryBtnLoading(isLoading); + }, [requestStatus]); + + useEffect(() => { + getMonitorData(); + }, []); + + const getMonitorData = () => { + setTotalEmpty(true) + dispatch({ type: ACTIONS.RESET }); + // ...根据时间和集群获取监控数据... + let query = queryData + if(!queryData || queryData.start === undefined){ + const { startTime, endTime } = getTime(timeButton, datePickerValue||[],) + query={...query,start: startTime, end: endTime } + } + const data:SearchBody = query! + setQueryData(data) + setRecordQuery({...data,timeButton}) + getPieData(data) + getInvokeData(data) + getMessageData(data) + monitorApiTableRef.current?.reload() + monitorSubTableRef.current?.reload() + }; + + const getPieData = (body: SearchBody) => { + fetchPieData(body) + .then((resp) => { + const {code,data,msg} = resp + setQueryBtnLoading(false) + if (code === STATUS_CODE.SUCCESS) { + setPieError(false) + setRequestStatic(data.requestSummary) + setProxyStatic(data.proxySummary) + setRequestPie({ 请求成功数: data.requestSummary.success, 请求失败数: data.requestSummary.fail }) + setProxyPie({ 转发成功数: data.proxySummary.success, 转发失败数: data.proxySummary.fail }) + setPieError(false) + // this.requestPieRef?.changePieChart() + // this.proxyPieRef?.changePieChart() + setRequestSucRate(data.requestSummary.total === 0 ? '0%' : (data.requestSummary.success * 100 / data.requestSummary.total).toFixed(2) + '%') + setProxySucRate(data.proxySummary.total === 0 ? '0%' : (data.proxySummary.success * 100 / data.proxySummary.total).toFixed(2) + '%') + setTotalEmpty(data.requestSummary.total === 0 && data.proxySummary.total === 0) + }else{ + setPieError(true) + message.error(msg || '获取数据失败,请重试') + } + }).finally(()=>{ + dispatch({ type: ACTIONS.REQUEST_COMPLETE, payload: 'getPieData' }); + }) + }; + + const getInvokeData = (body: SearchBody) => { + fetchInvokeData(body).then((resp) => { + const {code,data,msg} = resp + setQueryBtnLoading(false) + if (code === STATUS_CODE.SUCCESS) { + const { timeInterval, ...arr } = data + setInvokeStatic(arr as InvokeData) + setInvokeStaticError(false) + setTimeUnit(getTimeUnit(timeInterval!)) + // this.invokeLineRef?.changeLineChart() + }else{ + setInvokeStaticError(true) + message.error(msg || '获取数据失败,请重试') + } + }).finally(()=>{ + dispatch({ type: ACTIONS.REQUEST_COMPLETE, payload: 'getInvokeData' }); + }) + }; + + const getMessageData = (body: SearchBody) => { + fetchMessageData(body).then((resp) => { + const {code,data,msg} = resp + setQueryBtnLoading(false) + if (code === STATUS_CODE.SUCCESS) { + setTrafficStaticError(false) + setTrafficStatic(data) + // this.trafficLineRef?.changeLineChart() + }else{ + setTrafficStaticError(true) + message.error(msg || '获取数据失败,请重试') + } + }).finally(()=>{ + dispatch({ type: ACTIONS.REQUEST_COMPLETE, payload: 'getMessageData' }); + }) + }; + + + const getTablesData = (body: SearchBody,type: 'api' | 'subscriber'|'provider') => { + return fetchTableData(body,type).then((resp) => { + const {code,data,msg} = resp + setQueryBtnLoading(false) + if(code === STATUS_CODE.SUCCESS){ + return {data:data.top10.map((x:MonitorApiData | MonitorSubscriberData)=>{x.proxyRate = Number((x.proxyRate*100).toFixed(2));x.requestRate = Number((x.requestRate*100).toFixed(2));return x}), success: true} + }else{ + message.error(msg || '获取数据失败,请重试') + return {data:[], success:false} + } + }).catch(() => { + setQueryBtnLoading(false) + return {data:[], success:false} + }).finally(() => { + dispatch({ type: ACTIONS.REQUEST_COMPLETE, payload: 'getTablesData'}) + }) + }; + + + const resetQuery = () => { + // ...重置查询条件... + setTimeButton('hour') + setDatePickerValue(null) + setQueryData(undefined) + }; + + + const handleTimeRangeChange = (timeRange:TimeRange) => { + setQueryData(pre => ({...pre, ...timeRange} as SearchBody )) + }; + + + const monitorTopDataTabItems:TabsProps['items'] = [ + { + label:'API 请求量 Top10', + key:'api', + children:{getDetailData(record as MonitorApiData,'api')}} request={()=>getTablesData(queryData||{},'api')}/> + }, + { + label:'应用调用量 Top10', + key:'subscribers', + children:{getDetailData(record as MonitorSubscriberData,'subscriber')}} request={()=>getTablesData(queryData||{},'subscriber')} /> + }, + { + label:'服务被调用量 Top10', + key:'providers', + children:{getDetailData(record as MonitorSubscriberData,'provider')}} request={()=>getTablesData(queryData||{},'provider')} /> + } + ] + + const getDetailData = (entity:MonitorApiData|MonitorSubscriberData, type:'api'|'provider'|'subscriber')=>{ + setDetailEntityName(entity.name) + setDetailId(entity.id) + setDetailType(type) + setDrawerOpen(true) + } + + return ( +
+ {/* 筛选区域 */} + +
+ {/* 筛选集群 */} + {/*
+ + + + + + + + +
+ + + +
+ +
+
+ + +
+

Version {state.version}-{state.updateDate}

+

{state.powered}

+
+
+ + ); +} +export default Login; \ No newline at end of file diff --git a/frontend/packages/market/src/pages/serviceHub/ApiTestGroup.tsx b/frontend/packages/market/src/pages/serviceHub/ApiTestGroup.tsx new file mode 100644 index 00000000..e52acc9c --- /dev/null +++ b/frontend/packages/market/src/pages/serviceHub/ApiTestGroup.tsx @@ -0,0 +1,85 @@ + +import {Empty, Input} from "antd"; +import {debounce} from "lodash-es"; +import {SearchOutlined} from "@ant-design/icons"; +import {useEffect, useMemo, useState} from "react"; +import {DataNode} from "antd/es/tree"; +import {ApiDetail} from "@common/const/api-detail"; +import ApiTest from "@common/components/postcat/ApiTest.tsx"; +import DirectoryTree from "antd/es/tree/DirectoryTree"; + +type ApiTestGroupType = { + apiInfoList:ApiDetail[] + selectedApiId:string +} +export default function ApiTestGroup({apiInfoList,selectedApiId }:ApiTestGroupType){ + const [searchWord, setSearchWord] = useState('') + const [selectedApi,setSelectedApi] = useState([selectedApiId]) + const [selectedApiInfo, setSelectedApiInfo] = useState() + const onSearchWordChange = (e:unknown)=>{ + //console.log(e) + } + + useEffect(()=>{ + setSelectedApi([selectedApiId]) + },[selectedApiId] + ) + const treeData = useMemo(() => { + const loop = (data: ApiDetail[]): DataNode[] => + data?.map((item) => { + const strTitle = item.name as string; + const index = strTitle.indexOf(searchWord); + const beforeStr = strTitle.substring(0, index); + const afterStr = strTitle.slice(index + searchWord.length); + const title = + index > -1 ? ( + + {beforeStr} + {searchWord} + {afterStr} + + ) : ( + {strTitle} + ); + + return { + title, + key: item.id, + }; + }); + return loop(apiInfoList); + }, [searchWord,apiInfoList]); + + useEffect(()=>{ + apiInfoList && apiInfoList.length > 0 &&setSelectedApi([apiInfoList[0].id]) + },[apiInfoList]) + + useEffect(() => { + setSelectedApiInfo(selectedApi? apiInfoList.filter(x=>x.id === selectedApi[0])?.[0] || undefined : undefined) + }, [selectedApi]); + + return ( +
+
+ debounce(onSearchWordChange, 100)(e)} + allowClear placeholder="搜索分类或标签" + prefix={ { + onSearchWordChange(e) + }}/>}/> + } + className="hidden-switcher" + blockNode={true} + treeData={treeData} + selectedKeys={selectedApi} + onSelect={(selectedKeys) => { + setSelectedApi([selectedKeys[0] as string]) + }} /> +
+ {selectedApiInfo ? + : + + } +
+ ) +} \ No newline at end of file diff --git a/frontend/packages/market/src/pages/serviceHub/ApplyServiceModal.tsx b/frontend/packages/market/src/pages/serviceHub/ApplyServiceModal.tsx new file mode 100644 index 00000000..b742e74d --- /dev/null +++ b/frontend/packages/market/src/pages/serviceHub/ApplyServiceModal.tsx @@ -0,0 +1,75 @@ + +import { App, Form, Row, Col, Select, Input } from "antd"; +import { forwardRef, useEffect, useImperativeHandle, useMemo } from "react"; +import WithPermission from "@common/components/aoplatform/WithPermission"; +import { BasicResponse, STATUS_CODE } from "@common/const/const"; +import { ApplyServiceHandle, ApplyServiceProps } from "../../const/serviceHub/type"; +import { useFetch } from "@common/hooks/http"; + +export const ApplyServiceModal = forwardRef((props,ref)=>{ + const { message } = App.useApp() + const {entity,mySystemOptionList,reApply} = props + const [form] = Form.useForm(); + const {fetchData} = useFetch() + + useEffect(() => { + form.setFieldsValue(reApply ? {applications:entity?.app.id}:{}) + }, []); + + const apply: ()=>Promise = ()=>{ + return new Promise((resolve, reject)=>{ + form.validateFields().then((value)=>{ + fetchData>('catalogue/service/subscribe',{method:'POST',eoParams:{team:entity?.team?.id}, eoBody:({...value,service:entity.id})}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + useImperativeHandle(ref, ()=>({ + apply + }) + ) + + return ( +
+ +
服务名称: + {entity.name} + + + 服务 ID: + {entity.id} + + + } + // onSearch={handleTest} + /> + + { + apiDetail?.match && apiDetail.match?.length > 0 && + + } + + { + apiDetail?.proxy && Object.keys(apiDetail?.proxy).length > 0 && + + } + + {apiDetail && } + + // testClick(apiDocs.id)} entity={doc} /> + }]} + activeKey={activeKey} + onChange={(val)=>{setActiveKey(val as string[])}} + /> + + ))} + + + {/*
+
+

状态码

+ +
*/} + + + + document.getElementById('layout-ref')!} + items={category} + /> + + + + + 退出测试 + } + closeIcon={false} + > + + + + ) +} + +export default ServiceHubApiDocument \ No newline at end of file diff --git a/frontend/packages/market/src/pages/serviceHub/ServiceHubDetail.tsx b/frontend/packages/market/src/pages/serviceHub/ServiceHubDetail.tsx new file mode 100644 index 00000000..19a25885 --- /dev/null +++ b/frontend/packages/market/src/pages/serviceHub/ServiceHubDetail.tsx @@ -0,0 +1,171 @@ +import {Link, useNavigate, useParams} from "react-router-dom"; +import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx"; +import { App, Avatar, Button, Descriptions, Divider, Tabs} from "antd"; +import { useEffect, useRef, useState} from "react"; +import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import {DefaultOptionType} from "antd/es/cascader"; +import { ApplyServiceHandle, ServiceBasicInfoType, ServiceDetailType } from "../../const/serviceHub/type.ts"; +import { EntityItem } from "@common/const/type.ts"; +import { ApplyServiceModal } from "./ApplyServiceModal.tsx"; +import ServiceHubApiDocument from "./ServiceHubApiDocument.tsx"; +import { ApiFilled, ArrowLeftOutlined, LeftOutlined } from "@ant-design/icons"; +import { Typography } from 'antd'; +import { SimpleSystemItem } from "@core/const/system/type.ts"; +import { Icon } from "@iconify/react/dist/iconify.js"; + +const { Title, Text } = Typography; + +const ServiceHubDetail = ()=>{ + const {serviceId} = useParams(); + const {setBreadcrumb} = useBreadcrumb() + const [serviceBasicInfo, setServiceBasicInfo] = useState() + const [serviceName, setServiceName] = useState() + const [serviceDesc, setServiceDesc] = useState() + const [serviceDoc, setServiceDoc] = useState() + const {fetchData} = useFetch() + const applyRef = useRef(null) + const { modal,message } = App.useApp() + const [mySystemOptionList, setMySystemOptionList] = useState() + const [applied,setApplied] = useState(false) + const [activeKey, setActiveKey] = useState([]) + const [service, setService] = useState() + const navigate = useNavigate(); + + const getServiceBasicInfo = ()=>{ + fetchData>('catalogue/service',{method:'GET',eoParams:{service:serviceId}, eoTransformKeys:['app_num','api_num','update_time']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setService(data.service) + setServiceBasicInfo(data.service.basic) + setServiceName(data.service.name) + setServiceDesc(data.service.description) + setApplied(data.service.applied) + setServiceDoc(data.service.document) + setActiveKey(data.service.apis.map((x)=>x.id)) + }else{ + message.error(msg || '操作失败') + } + }) + } + + useEffect(() => { + if(!serviceId){ + console.warn('缺少serviceId') + return + } + serviceId && getServiceBasicInfo() + }, [serviceId]); + + useEffect(() => { + getMySelectList() + setBreadcrumb( + [ + {title:服务市场}, + {title:'服务详情'} + ] + ) + + }, []); + + + const getMySelectList = ()=>{ + setMySystemOptionList([]) + fetchData>('simple/apps/mine',{method:'GET'}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setMySystemOptionList(data.apps?.map((x:SimpleSystemItem)=>{return { + label:x.name, value:x.id + }})) + }else{ + message.error(msg || '操作失败') + } + }) + } + + + const openModal = (type:'apply')=>{ + modal.confirm({ + title:'申请服务', + content:, + onOk:()=>{ + return applyRef.current?.apply().then((res)=>{ + if(res === true) setApplied(true) + }) + }, + okText:'确认', + cancelText:'取消', + closable:true, + icon:<>, + width:600 + }) + } + + const items = [ + { + key: 'introduction', + label: '介绍', + children: <>
,
+            icon: ,
+        },
+        {
+            key: 'api-document',
+            label: 'API 文档',
+            children: 
, + icon: + } + ] + + return ( +
+
+
+ +
+ +
+
+ {/* {service?.name?.substring(0,1)} */} + : undefined} + icon={serviceBasicInfo?.logo ? '' :}> + +
+

{serviceName}

+
+ {serviceDesc || '-'} +
+ +
+
+
+
+
+ + +
+
+ + {serviceBasicInfo?.appNum ?? '-'} + {serviceBasicInfo?.team?.name || '-'} + {serviceBasicInfo?.catalogue?.name || '-'} + {serviceBasicInfo?.tags?.map(x=>x.name)?.join(',') || '-'} + + + + { serviceBasicInfo?.version || '-'} + {serviceBasicInfo?.updateTime || '-'} + +
+
+ ) +} + +export default ServiceHubDetail \ No newline at end of file diff --git a/frontend/packages/market/src/pages/serviceHub/ServiceHubGroup.tsx b/frontend/packages/market/src/pages/serviceHub/ServiceHubGroup.tsx new file mode 100644 index 00000000..51ef2dfa --- /dev/null +++ b/frontend/packages/market/src/pages/serviceHub/ServiceHubGroup.tsx @@ -0,0 +1,117 @@ +import {debounce} from "lodash-es"; +import {SearchOutlined} from "@ant-design/icons"; +import {App, Divider, Input, TreeDataNode} from "antd"; +import {useCallback, useEffect, useState} from "react"; +import Tree, {DataNode} from "antd/es/tree"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import { CategorizesType, TagType } from "../../const/serviceHub/type.ts"; +import { filterServiceList, initialServiceHubListState, SERVICE_HUB_LIST_ACTIONS, ServiceHubListActionType } from "./ServiceHubList.tsx"; + +type ServiceHubGroup = { + children:JSX.Element + filterOption:typeof initialServiceHubListState + dispatch:React.Dispatch +} + +export const ServiceHubGroup = ({children,filterOption,dispatch}:ServiceHubGroup)=>{ + const {message} = App.useApp() + const {fetchData} = useFetch() + + useEffect(() => { + getTagAndServiceClassifyList() + }, []); + + const onSearchWordChange = (e:string)=>{ + dispatch({type:SERVICE_HUB_LIST_ACTIONS.SET_KEYWORD,payload:e}) + dispatch({type:SERVICE_HUB_LIST_ACTIONS.LIST_LOADING,payload:true}) + dispatch({type:SERVICE_HUB_LIST_ACTIONS.SET_SERVICES,payload: filterServiceList({...filterOption,keyword:e})}) + dispatch({type:SERVICE_HUB_LIST_ACTIONS.LIST_LOADING,payload:false}) + } + + const getTagAndServiceClassifyList = ()=>{ + fetchData>('catalogues',{method:'GET'}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + dispatch({type:SERVICE_HUB_LIST_ACTIONS.GET_CATEGORIES,payload:data.catalogues}) + dispatch({type:SERVICE_HUB_LIST_ACTIONS.GET_TAGS,payload:[...data.tags,{id:'empty',name:'无标签'}]}) + dispatch({type:SERVICE_HUB_LIST_ACTIONS.SET_SELECTED_CATE,payload:[...data.catalogues.map((x:CategorizesType)=>x.id)]}) + dispatch({type:SERVICE_HUB_LIST_ACTIONS.SET_SELECTED_TAG,payload:[...data.tags.map((x:TagType)=>x.id),'empty']}) + }else{ + message.error(msg || '操作失败') + } + }) + } + + const transferToTreeData = useCallback((data:CategorizesType[] | TagType[] ):TreeDataNode[]=>{ + const loop = (data: CategorizesType[] | TagType[] ): DataNode[] => + data?.map((item) => { + if ((item as CategorizesType).children) { + return { + title:item.name, + key: item.id, children: loop((item as CategorizesType).children) + }; + } + return { + title:item.name, + key: item.id, + }; + }); + return loop(data || []) + },[]) + + const onCheckHandler = (type: 'SET_SELECTED_CATE' | 'SET_SELECTED_TAG' | 'SET_SELECTED_PARTITION') => (checkedKeys:string[]) => { + dispatch({ type: SERVICE_HUB_LIST_ACTIONS[type], payload: checkedKeys }); + dispatch({type:SERVICE_HUB_LIST_ACTIONS.LIST_LOADING,payload:true}) + + dispatch({type:SERVICE_HUB_LIST_ACTIONS.SET_SERVICES,payload: filterServiceList({...filterOption,[(type === 'SET_SELECTED_CATE' ? 'selectedCate' : type === 'SET_SELECTED_TAG' ? 'selectedTag' : 'selectedPartition' ) as keyof typeof filterOption]: checkedKeys })}) + dispatch({type:SERVICE_HUB_LIST_ACTIONS.LIST_LOADING,payload:false}) + }; + + + return ( +
+
+
+ debounce(onSearchWordChange, 500)(e.target.value)} + allowClear placeholder="搜索服务" + prefix={}/> +
+
+

分类

+ x.children && x.children.length > 0).length > 0 ? '' : 'no-first-switch-tree'}`} + checkable + blockNode={true} + checkedKeys={filterOption.selectedCate} + onCheck={onCheckHandler('SET_SELECTED_CATE')} + treeData={transferToTreeData(filterOption.categoriesList)} + showIcon={false} + selectable={false} + /> +
+ +
+

标签

+ +
+
+
+
+
+ {children} +
+
); +} + +export default ServiceHubGroup diff --git a/frontend/packages/market/src/pages/serviceHub/ServiceHubList.tsx b/frontend/packages/market/src/pages/serviceHub/ServiceHubList.tsx new file mode 100644 index 00000000..13ec5421 --- /dev/null +++ b/frontend/packages/market/src/pages/serviceHub/ServiceHubList.tsx @@ -0,0 +1,201 @@ +import {ActionType } from "@ant-design/pro-components"; +import {FC, forwardRef, useEffect, useReducer, useRef} from "react"; +import { useNavigate, useParams} from "react-router-dom"; +import {App,Card, Avatar, Tag, Empty, Spin, Tooltip} from "antd"; +import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx"; +import { CategorizesType, ServiceHubTableListItem, TagType } from "../../const/serviceHub/type.ts"; +import { VirtuosoGrid } from 'react-virtuoso'; +import { ApiOutlined,LoadingOutlined } from "@ant-design/icons"; +import ServiceHubGroup from "./ServiceHubGroup.tsx"; +import { unset } from "lodash-es"; + +export enum SERVICE_HUB_LIST_ACTIONS { + GET_CATEGORIES = 'GET_CATEGORIES', + GET_TAGS ='GET_TAGS', + GET_SERVICES = 'GET_SERVICES', + SET_SERVICES='SET_SERVICES', + SET_SELECTED_CATE = 'SET_SELECTED_CATE', + SET_SELECTED_TAG = 'SET_SELECTED_TAG', + SET_SELECTED_PARTITION = 'SET_SELECTED_PARTITION', + SET_KEYWORD = 'SET_KEYWORD', + LIST_LOADING = 'LIST_LOADING' + } + +export type ServiceHubListActionType = +| { type: SERVICE_HUB_LIST_ACTIONS.GET_CATEGORIES, payload: CategorizesType[] } +| { type: SERVICE_HUB_LIST_ACTIONS.GET_TAGS, payload: TagType[] } +| { type: SERVICE_HUB_LIST_ACTIONS.GET_SERVICES, payload: ServiceHubTableListItem[] } +| { type: SERVICE_HUB_LIST_ACTIONS.SET_SERVICES, payload: ServiceHubTableListItem[] } +| { type: SERVICE_HUB_LIST_ACTIONS.SET_SELECTED_CATE, payload: string[] } +| { type: SERVICE_HUB_LIST_ACTIONS.SET_SELECTED_TAG, payload: string[] } +| { type: SERVICE_HUB_LIST_ACTIONS.SET_SELECTED_PARTITION, payload: string[] } +| { type: SERVICE_HUB_LIST_ACTIONS.SET_KEYWORD, payload: string } +| { type: SERVICE_HUB_LIST_ACTIONS.LIST_LOADING, payload: boolean } + +export const initialServiceHubListState = { + categoriesList: [] as CategorizesType[], + tagsList: [] as TagType[], + servicesList: [] as ServiceHubTableListItem[], + showServicesList: [] as ServiceHubTableListItem[], + selectedCate: [] as string[], + selectedTag: [] as string[], + selectedPartition: [] as string[], + keyword: '', + getCateAndTagData:false, + getPartitionData:false, + listLoading:false, + }; + + function reducer(state: typeof initialServiceHubListState, action: ServiceHubListActionType) { + switch (action.type) { + case SERVICE_HUB_LIST_ACTIONS.GET_CATEGORIES: + return { ...state, categoriesList: action.payload , getCateAndTagData:true}; + case SERVICE_HUB_LIST_ACTIONS.GET_TAGS: + return { ...state, tagsList: action.payload , getCateAndTagData:true}; + case SERVICE_HUB_LIST_ACTIONS.GET_SERVICES: + return { ...state, servicesList: action.payload }; + case SERVICE_HUB_LIST_ACTIONS.SET_SERVICES: + return { ...state, showServicesList: action.payload }; + case SERVICE_HUB_LIST_ACTIONS.SET_SELECTED_CATE: + return { ...state, selectedCate: action.payload }; + case SERVICE_HUB_LIST_ACTIONS.SET_SELECTED_TAG: + return { ...state, selectedTag: action.payload }; + case SERVICE_HUB_LIST_ACTIONS.SET_SELECTED_PARTITION: + return { ...state, selectedPartition: action.payload }; + case SERVICE_HUB_LIST_ACTIONS.SET_KEYWORD: + return { ...state, keyword: action.payload }; + case SERVICE_HUB_LIST_ACTIONS.LIST_LOADING: + return { ...state, listLoading: action.payload }; + default: + return state; + } + } + + export const filterServiceList = (dataSet: typeof initialServiceHubListState)=>{ + if(!dataSet.getCateAndTagData || !dataSet.getPartitionData){ + return dataSet.servicesList + }else{ + return dataSet.servicesList.filter((x)=>{ + if(!dataSet.selectedCate || dataSet.selectedCate.length === 0 || dataSet.selectedCate.indexOf(x.catalogue.id) === -1) return false + if(!dataSet.selectedTag || dataSet.selectedTag.length === 0) return false + if((!x.tags || !x.tags.length )&& dataSet.selectedTag.indexOf('empty') === -1) return false + if(x.tags && x.tags.length && !x.tags.some(tag => dataSet.selectedTag.includes(tag.id))) return false; + if(!dataSet.selectedPartition || dataSet.selectedPartition.length === 0) return false + if( dataSet.keyword && !x.name.includes(dataSet.keyword)) return false + return true + }) + } +} + +const ServiceHubList:FC = ()=>{ + const { setBreadcrumb} = useBreadcrumb() + const { message } = App.useApp() + const {fetchData} = useFetch() + const { categoryId, tagId} = useParams() + const pageListRef = useRef(null); + // const callbackUrl = new URLSearchParams(window.location.search).get('callbackUrl'); + const navigate = useNavigate() + const [filterOption, dispatch] = useReducer(reducer, initialServiceHubListState) + + const getServiceList = ()=>{ + dispatch({type:SERVICE_HUB_LIST_ACTIONS.LIST_LOADING,payload:true}) + fetchData>('catalogue/services',{method:'GET',eoTransformKeys:['api_num','subscriber_num']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + dispatch({type:SERVICE_HUB_LIST_ACTIONS.GET_SERVICES,payload:data.services}) + dispatch({type:SERVICE_HUB_LIST_ACTIONS.SET_SERVICES,payload: filterServiceList({...filterOption, servicesList:data.services})}) + + }else{ + message.error(msg || '操作失败') + } + }).finally(()=>{ dispatch({type:SERVICE_HUB_LIST_ACTIONS.LIST_LOADING,payload:false})}) + } + + const showDocumentDetail = (entity:ServiceHubTableListItem)=>{ + navigate(`../detail/${entity.id}`) + } + + useEffect(() => { + pageListRef.current?.reload() + }, [categoryId,tagId]); + useEffect(() => { + setBreadcrumb( + [ + {title:'服务市场'} + ] + ) + getServiceList() + }, []); + + return ( + +
+ } spinning={filterOption.listLoading}> + {filterOption.showServicesList && filterOption.showServicesList.length > 0 ? { + const item = filterOption.showServicesList[index]; + return ( +
+ showDocumentDetail(item)}> + {item.description || '暂无服务描述'} + +
+ ); + }} + components={{ + List: forwardRef(({ style, children, ...props }, ref) => ( +
+ {children} +
+ )), + Item: ({ children, ...props }) => ( + <> + {children} + ) + }} + />:} +
+
+
+ ) + +} +export default ServiceHubList + +const CardTitle = (service:ServiceHubTableListItem)=>{ + return( +
+ : undefined}> {service.logo ? '' : service.name.substring(0,1)} +
+

{service.name}

+
+ {service.catalogue?.name || '-'} + + + {service.apiNum ?? '-'} + + + {service.subscriberNum ?? '-'} + +
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/packages/market/src/pages/serviceHub/management/ApprovalModalContent.tsx b/frontend/packages/market/src/pages/serviceHub/management/ApprovalModalContent.tsx new file mode 100644 index 00000000..e54d7c09 --- /dev/null +++ b/frontend/packages/market/src/pages/serviceHub/management/ApprovalModalContent.tsx @@ -0,0 +1,93 @@ + +import { App, Form, Row, Col, Input } from "antd" +import { forwardRef, useImperativeHandle, useEffect } from "react" +import WithPermission from "@common/components/aoplatform/WithPermission" +import { BasicResponse, STATUS_CODE } from "@common/const/const" +import { useFetch } from "@common/hooks/http" +import { SYSTEM_SUBSCRIBE_APPROVAL_DETAIL_LIST } from "@core/const/system/const" +import { SubSubscribeApprovalModalHandle, SubSubscribeApprovalModalProps } from "@core/const/system/type" + +type FieldType = { + reason: string + opinion?:string +} + +export const ApprovalModalContent = forwardRef((props, ref) => { + const { message } = App.useApp() + const {data, type, serviceId, teamId} = props + const [form] = Form.useForm(); + const {fetchData} = useFetch() + + const reApply:()=>Promise = ()=>{ + return new Promise((resolve, reject)=>{ + if(type === 'view'){ + resolve(true) + return + } + form.validateFields().then((value)=>{ + fetchData>('catalogue/service/subscribe',{method: 'POST',eoParams:{team:teamId}, eoBody:({service:data!.service.id, applications:[serviceId], reason:value.reason})}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + useImperativeHandle(ref, ()=>({ + reApply + }) + ) + + useEffect(()=>{ + form.setFieldsValue({...data}) + },[]) + + + return ( +
+ + + + {SYSTEM_SUBSCRIBE_APPROVAL_DETAIL_LIST?.map((x)=>{ + return ( + +
{x.title}: + {/* {showData(x)} */} + {x.nested ? data?.[x.key]?.[x.nested] : ( (data as {[k:string]:unknown})?.[x.key] || '-')} + ) + })} + + + label="申请原因" + name="reason" + > + + + + label="审核意见" + name="opinion" + > + + + + + + ) +}) \ No newline at end of file diff --git a/frontend/packages/market/src/pages/serviceHub/management/ManagementAppSetting.tsx b/frontend/packages/market/src/pages/serviceHub/management/ManagementAppSetting.tsx new file mode 100644 index 00000000..e995f4e8 --- /dev/null +++ b/frontend/packages/market/src/pages/serviceHub/management/ManagementAppSetting.tsx @@ -0,0 +1,16 @@ +import { RouterParams } from "@core/components/aoplatform/RenderRoutes"; +import { useParams } from "react-router-dom"; +import ManagementConfig from "./ManagementConfig"; + +export default function ManagementAppSetting(){ + const {teamId,appId} = useParams() + + return ( +
+
应用管理
+
+ +
+
+ ) +} \ No newline at end of file diff --git a/frontend/packages/market/src/pages/serviceHub/management/ManagementAuthorityConfig.tsx b/frontend/packages/market/src/pages/serviceHub/management/ManagementAuthorityConfig.tsx new file mode 100644 index 00000000..142dabd6 --- /dev/null +++ b/frontend/packages/market/src/pages/serviceHub/management/ManagementAuthorityConfig.tsx @@ -0,0 +1,261 @@ +import {forwardRef, useEffect, useImperativeHandle, useState} from "react"; +import {App, Checkbox, Form, Input, Select,Switch} from "antd"; +import moment from "moment"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import DatePicker from "@common/components/aoplatform/DatePicker.tsx"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import { v4 as uuidv4} from 'uuid'; +import { ALGORITHM_ITEM } from "@core/const/system/const.tsx"; +import { EditAuthFieldType } from "@core/const/system/type"; + +export type ManagementAuthorityConfigProps = { + type:'add'|'edit' + data?:EditAuthFieldType + appId:string + teamId:string +} + +export type ManagementAuthorityConfigHandle = { + save:()=>Promise +} + +export const ManagementAuthorityConfig = forwardRef((props,ref)=>{ + const { message } = App.useApp() + const {type, data,appId, teamId} = props + const [form] = Form.useForm(); + const [driver, setDriver]=useState('basic') + const [algorithm, setAlgorithm] = useState('HS256') + const [, forceUpdate] = useState(null); + const {fetchData} = useFetch() + + const save :()=>Promise = ()=>{ + return new Promise((resolve, reject)=>{ + form.validateFields().then((value)=>{ + fetchData>('app/authorization',{method:type === 'add'? 'POST' : 'PUT',eoBody:({...value,expireTime:value.expireTime ? value.expireTime.unix() : 0}), eoParams:type === 'add' ? {app:appId,team:teamId}:{authorization:data!.id,app:appId,team:teamId},eoTransformKeys:['hideCredential','expireTime','tokenName','userName']}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }).catch((errorInfo)=> reject(errorInfo)) + })} + + useImperativeHandle(ref, ()=>({ + save + }) + ) + + const prefixSelector = ( + + + + ); + + const onAlgorithmChange = (algorithm:string)=>{ + setAlgorithm(algorithm) + } + + const onDriverChange = (driver:string)=>{ + setDriver(driver) + if(driver === 'jwt' && !form.getFieldValue(['config','algorithm'])){ + form.setFieldValue(['config','algorithm'],'HS256') + forceUpdate({}) + } + } + + + const disabledDate = (current: moment.Moment | null): boolean => { + // 禁用今天以前的日期,包括今天 + // 使用current?.startOf('day')是为了获取日期的开始时间点,以确保整个今天都被禁用 + // 如果只需要禁用今天之前的日期(今天可选),则可以将`isBefore`的第二个参数设置为 'day' + return current ? current.startOf('day') < moment().startOf('day') : false; + }; + + + useEffect(() => { + //console.log(data) + if(type === 'edit' && data){ + form.setFieldsValue({...data,expireTime:data.expireTime === 0 ? '' : moment(data.expireTime *1000)}) + forceUpdate({}) + }else{ + form.setFieldsValue({driver, position:'Header',tokenName:'Authorization'}) + form.setFieldValue(['config','userName'],uuidv4()) + form.setFieldValue(['config','password'],uuidv4()) + form.setFieldValue(['config','apikey'],uuidv4()) + forceUpdate({}) + } + }, []); + + return ( + // +
+ + label="名称" + name="name" + rules={[{required: true, message: '必填项',whitespace:true }]} + > + + + + + label="鉴权类型" + name="driver" + rules={[{required: true, message: '必填项'}]} + > + + + + {(()=>{ + switch(form.getFieldValue('driver')){ + case 'basic': + return <> + + label="用户名" + name={['config','userName']} + rules={[{required: true, message: '必填项',whitespace:true }]} + > + + + + + label="密码" + name={['config','password']} + rules={[{required: true, message: '必填项',whitespace:true }]} + > + + + + case 'jwt': + return <> + + label="Iss" + name={['config','iss']} + rules={[{required: true, message: '必填项'}]} + > + + + + + label="签名算法" + name={['config','algorithm']} + rules={[{required: true, message: '必填项'}]} + > + + + + + label="用户名" + name={['config','user']} + > + + + + + label="用户名 JsonPath" + name={['config','userPath']} + > + + + + + label="校验字段" + name={['config','claimsToVerify']} + > + + + + + label="SK" + name={['config','sk']} + rules={[{required: true, message: '必填项'}]} + > + + + + case 'apikey': + return <> + + label="Apikey" + name={['config','apikey']} + rules={[{required: true, message: '必填项',whitespace:true }]} + > + + + + } + + })()} + + + label="过期时间" + name="expireTime" + > + + + + + label="隐藏鉴权信息" + name="hideCredential" valuePropName="checked" + > + + + + //
+ ); +}) \ No newline at end of file diff --git a/frontend/packages/market/src/pages/serviceHub/management/ManagementAuthorityView.tsx b/frontend/packages/market/src/pages/serviceHub/management/ManagementAuthorityView.tsx new file mode 100644 index 00000000..f5133a81 --- /dev/null +++ b/frontend/packages/market/src/pages/serviceHub/management/ManagementAuthorityView.tsx @@ -0,0 +1,26 @@ +import {Col, Row} from "antd"; +import {useEffect, useState} from "react"; + +export type ManagementAuthorityViewProps = { + entity:Array<{key:string, value:string}> +} + +export const ManagementAuthorityView = ({entity}:ManagementAuthorityViewProps)=>{ + const [detail,setDetail] = useState>(entity) + + useEffect(() => { + setDetail(entity) + }, [entity]); + + return ( +
{ + detail?.length > 0 && detail.map((k,i)=>( + +
{k.key}: + { k.value || '-'} + + )) + } + + ) +} \ No newline at end of file diff --git a/frontend/packages/market/src/pages/serviceHub/management/ManagementConfig.tsx b/frontend/packages/market/src/pages/serviceHub/management/ManagementConfig.tsx new file mode 100644 index 00000000..19b74420 --- /dev/null +++ b/frontend/packages/market/src/pages/serviceHub/management/ManagementConfig.tsx @@ -0,0 +1,185 @@ + +import {App, Button, Divider, Form, Input, Row} from "antd"; +import {forwardRef, useEffect, useImperativeHandle, useState} from "react"; +import {v4 as uuidv4} from 'uuid' +import WithPermission from "@common/components/aoplatform/WithPermission"; +import { BasicResponse, STATUS_CODE } from "@common/const/const"; +import { useFetch } from "@common/hooks/http"; +import { useNavigate } from "react-router-dom"; +import { useTenantManagementContext } from "@market/contexts/TenantManagementContext"; + +export type ManagementConfigFieldType = { + name:string + description:string + id?:string + team?:string + asApp?:boolean +}; + +type ManagementConfigProps = { + type:'add'|'edit' + teamId:string + appId?:string +} + +export type ManagementConfigHandle = { + save:()=>Promise +} + + +const ManagementConfig = forwardRef((props, ref) => { + const { message,modal } = App.useApp() + const {type,teamId,appId} = props + const [form] = Form.useForm(); + const {fetchData} = useFetch() + const [delBtnLoading, setDelBtnLoading] = useState(false) + const {setAppName} = type === 'edit' ? useTenantManagementContext():{setAppName:()=>{}} + const navigate = type === 'edit' ? useNavigate() : ()=>{} + const save:()=>Promise = ()=>{ + return new Promise((resolve, reject)=>{ + form.validateFields().then((value)=>{ + fetchData>(type === 'add'? 'team/app' : 'app/info',{method:type === 'add'? 'POST' : 'PUT',eoBody:(value), eoParams:type === 'add' ? {team:teamId}:{app:appId,team:teamId}}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + form.setFieldsValue(data.apps) + type === 'edit' && setAppName(data.apps.name) + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + // 获取表单默认值 + const getApplicationInfo = () => { + fetchData>('app/info',{method:'GET',eoParams:{app:appId,team:teamId},eoTransformKeys:['as_app']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setAppName(data.app.name) + setTimeout(()=>{form.setFieldsValue({...data.app})},0) + }else{ + message.error(msg || '操作失败') + } + }) + }; + + const deleteApplicationModal = async ()=>{ + setDelBtnLoading(true) + modal.confirm({ + title:'删除', + content:'该数据删除后将无法找回,请确认是否删除?', + onOk:()=> { + return deleteApplication() + }, + width:600, + okText:'确认', + okButtonProps:{ + danger:true + }, + onCancel:()=>{ + setDelBtnLoading(false) + }, + cancelText:'取消', + closable:true, + icon:<> + }) + } + + + const deleteApplication = ()=>{ + fetchData>('app',{method:'DELETE',eoParams:{app:appId,team:teamId}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + navigate(`/tenantManagement/list`) + }else{ + message.error(msg || '操作失败') + } + }) + } + + useImperativeHandle(ref, ()=>({ + save + }) + ) + + useEffect(() => { + if(type === 'edit'){ + appId && getApplicationInfo() + }else{ + form.setFieldValue('id',uuidv4()) + } + }, [appId]); + + return (<> +
+
+ + label="应用名称" + name="name" + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + + + + label="应用 ID" + name="id" + extra="应用ID(app_id)可用于检索服务或日志" + rules={[{ required: true, message: '必填项' ,whitespace:true }]} + > + + + + + + + {type === 'edit' && <> + + + + + + }
+ + { type === 'edit' && <> + +
+

删除应用:删除操作不可恢复,请谨慎操作!

+
+ + + +
+
+ } + +
+ ) +}) + +export default ManagementConfig \ No newline at end of file diff --git a/frontend/packages/market/src/pages/serviceHub/management/ManagementInsideAuth.tsx b/frontend/packages/market/src/pages/serviceHub/management/ManagementInsideAuth.tsx new file mode 100644 index 00000000..db873778 --- /dev/null +++ b/frontend/packages/market/src/pages/serviceHub/management/ManagementInsideAuth.tsx @@ -0,0 +1,202 @@ +import { MoreOutlined } from "@ant-design/icons" +import { message, Card, Button, Tag, Dropdown, App, Empty } from "antd" +import { useState, useEffect, forwardRef, useRef } from "react" +import { VirtuosoGrid } from "react-virtuoso" +import { BasicResponse, STATUS_CODE } from "@common/const/const" +import { useBreadcrumb } from "@common/contexts/BreadcrumbContext" +import { useFetch } from "@common/hooks/http" +import { EditAuthFieldType, SystemAuthorityTableListItem } from "@core/const/system/type" +import { Link, useParams } from "react-router-dom" +import { RouterParams } from "@core/components/aoplatform/RenderRoutes" +import moment from "moment" +import { useTenantManagementContext } from "../../../contexts/TenantManagementContext" +import { ManagementAuthorityConfig, ManagementAuthorityConfigHandle } from "./ManagementAuthorityConfig" +import { ManagementAuthorityView } from "./ManagementAuthorityView" +import { checkAccess } from "@common/utils/permission" +import { useGlobalContext } from "@common/contexts/GlobalStateContext" + +export default function ManagementInsideAuth(){ + const {modal} = App.useApp() + const {fetchData} = useFetch() + const [authList, setAuthList] = useState([]) + const {appId,teamId} = useParams() + const addRef = useRef(null) + const editRef = useRef(null) + const {appName} = useTenantManagementContext() + const {accessData} = useGlobalContext() + + + const getSystemAuthority = ()=>{ + return fetchData>('app/authorizations',{method:'GET',eoParams:{app:appId, team:teamId},eoTransformKeys:['hide_credential','create_time','update_time','expire_time']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setAuthList(data.authorizations) + }else{ + message.error(msg || '操作失败') + } + }).catch(() => { + return {data:[], success:false} + }) + } + + useEffect(() => { + getSystemAuthority() + }, []); + + + const deleteAuthority = (entity:SystemAuthorityTableListItem)=>{ + return new Promise((resolve, reject)=>{ + fetchData>('app/authorization',{method:'DELETE',eoParams:{authorization:entity!.id,app:appId, team:teamId}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + const openModal =async (type:'view'|'delete'|'add'|'edit',entity?:SystemAuthorityTableListItem)=>{ + //console.log(type,entity) + let title:string = '' + let content:string|React.ReactNode = '' + switch (type){ + case 'view':{ + title='鉴权详情' + message.loading('正在加载数据') + const {code,data,msg} = await fetchData>('app/authorization/details',{method:'GET',eoParams:{authorization:entity!.id,app:appId, team:teamId}}) + message.destroy() + if(code === STATUS_CODE.SUCCESS){ + content= + }else{ + message.error(msg || '操作失败') + return + }} + break; + case 'add': + title='添加鉴权' + content= + break; + case 'edit':{ + title='编辑鉴权' + message.loading('正在加载数据') + const {code,data,msg} = await fetchData>('app/authorization',{method:'GET',eoParams:{authorization:entity!.id,app:appId, team:teamId},eoTransformKeys:['hide_credential','token_name','expire_time','user_name','public_key','user_path','claims_to_verify','signature_is_base64']}) + message.destroy() + if(code === STATUS_CODE.SUCCESS){ + content= + }else{ + message.error(msg || '操作失败') + return + }} + break; + case 'delete': + title='删除' + content='该数据删除后将无法找回,请确认是否删除?' + break; + } + + modal.confirm({ + title, + content, + onOk:()=>{ + switch (type){ + case 'add': + return addRef.current?.save().then((res)=>{if(res === true) getSystemAuthority()}) + case 'edit': + return editRef.current?.save().then((res)=>{if(res === true) getSystemAuthority()}) + case 'delete': + return deleteAuthority(entity!).then((res)=>{if(res === true) getSystemAuthority()}) + case 'view': + return true + } + }, + width:600, + okText: '确认', + okButtonProps:{ + disabled : !checkAccess( `team.application.authorization.${type}`, accessData) + }, + cancelText:type === 'view'? '关闭':'取消', + closable:true, + icon:<>, + footer:(_, { OkBtn, CancelBtn }) =>{ + return(<> + + {type !== 'view' && } + ) + } + + }) + } + + + const dropdownMenu = (entity:SystemAuthorityTableListItem) => [ + { + key: 'edit', + label: ( + // + + // + ), + }, + { + key: 'delete', + label: ( + // + + // + ), + }, + ] + + return (
+
访问授权
+
+ {authList && authList.length > 0 ? { + const item = authList[index]; + return ( +
+
{item.name}
+ +
+
+ {`${item.driver.substring(0,1).toLocaleUpperCase()}${item.driver.substring(1)}`} + {item.expireTime === 0 ? '永不过期' : `到期时间:${moment(item.expireTime * 1000).format('YYYY-MM-DD hh:mm:ss')}`}
+
+
+ ); + }} + components={{ + List: forwardRef(({ style, children, ...props }, ref) => ( +
+ {children} +
+ )), + Item: ({ children, ...props }) => ( + <> + {children} + ) + }} + /> :
} +
) +} \ No newline at end of file diff --git a/frontend/packages/market/src/pages/serviceHub/management/ManagementInsidePage.tsx b/frontend/packages/market/src/pages/serviceHub/management/ManagementInsidePage.tsx new file mode 100644 index 00000000..05bac89e --- /dev/null +++ b/frontend/packages/market/src/pages/serviceHub/management/ManagementInsidePage.tsx @@ -0,0 +1,96 @@ + +import { ArrowLeftOutlined, LoadingOutlined } from "@ant-design/icons"; +import { App, Button, Menu, MenuProps, Spin } from "antd"; +import { useState, useEffect, useMemo } from "react"; +import { Link, Outlet, useLocation, useNavigate, useParams } from "react-router-dom"; +import { BasicResponse, STATUS_CODE } from "@common/const/const"; +import { useBreadcrumb } from "@common/contexts/BreadcrumbContext"; +import { useFetch } from "@common/hooks/http"; +import { ItemType } from "antd/es/breadcrumb/Breadcrumb"; +import { TENANT_MANAGEMENT_APP_MENU } from "../../../const/serviceHub/const"; +import { RouterParams } from "@core/components/aoplatform/RenderRoutes"; +import { useTenantManagementContext } from "@market/contexts/TenantManagementContext"; +import { ManagementConfigFieldType } from "./ManagementConfig"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext"; + +export default function ManagementInsidePage(){ + const { message } = App.useApp() + const {fetchData} = useFetch() + const { setBreadcrumb} = useBreadcrumb() + const [activeMenu, setActiveMenu] = useState('service') + const {appId,teamId} = useParams() + const navigateTo = useNavigate() + const currentUrl = useLocation().pathname + const [openKeys, setOpenKeys] = useState([]) + const [loading, setLoading] = useState(false) + const {appName,setAppName} = useTenantManagementContext() + const {getTeamAccessData,cleanTeamAccessData} = useGlobalContext() + + const menuData = useMemo(()=>{ + return TENANT_MANAGEMENT_APP_MENU + },[]) + + useEffect(()=>{ + setActiveMenu(currentUrl.split('/').pop() || 'service') + },[currentUrl]) + + const onMenuClick: MenuProps['onClick'] = (node) => { + setActiveMenu(node.key) + navigateTo(`/tenantManagement/${teamId}/inside/${appId}/${node.key}`) + }; + + useEffect(()=>{ + const fetchDataAsync = async () => { + let _appName = appName + if(appId && !appName && !currentUrl.includes('setting')){ + const {code,data} = await fetchData>('app/info',{method:'GET',eoParams:{app:appId,team:teamId},eoTransformKeys:['as_app']}) + if(code === STATUS_CODE.SUCCESS){ + _appName = data.app.name + setAppName(_appName) + } + } + setBreadcrumb( + [ + {title:应用}, + ...(_appName ? [{title:_appName}] : []) + ] + ) + }; + fetchDataAsync(); + }, + [appId,appName]) + + +useEffect(()=>{ + if(teamId ){ + getTeamAccessData(teamId) + } + return ()=>{ + cleanTeamAccessData() + } +},[teamId]) + + return (<> + } spinning={loading}> +
+
+
+ +
+ {setOpenKeys(e)}} + className="h-[calc(100%-59px)] overflow-auto" + style={{ width: 220}} + selectedKeys={[activeMenu!]} + mode="inline" + items={menuData as unknown as ItemType[] } + /> +
+
+ {}}}> +
+
+
) +} \ No newline at end of file diff --git a/frontend/packages/market/src/pages/serviceHub/management/ManagementInsideService.tsx b/frontend/packages/market/src/pages/serviceHub/management/ManagementInsideService.tsx new file mode 100644 index 00000000..9a9305d9 --- /dev/null +++ b/frontend/packages/market/src/pages/serviceHub/management/ManagementInsideService.tsx @@ -0,0 +1,213 @@ +import { MoreOutlined, SearchOutlined } from "@ant-design/icons" +import { Card, Input,Button ,Dropdown,App, Tag, Empty } from "antd" +import { debounce } from "lodash-es" +import { forwardRef, useEffect, useState } from "react" +import { VirtuosoGrid } from "react-virtuoso" +import { BasicResponse, STATUS_CODE } from "@common/const/const" +import { useBreadcrumb } from "@common/contexts/BreadcrumbContext" +import { useFetch } from "@common/hooks/http" +import { SubscribeApprovalInfoType } from "@common/const/approval/type" +import { Link, useNavigate, useOutletContext, useParams } from "react-router-dom" +import { RouterParams } from "@core/components/aoplatform/RenderRoutes" +import { TenantManagementServiceListItem } from "../../../const/serviceHub/type" +import { useTenantManagementContext } from "../../../contexts/TenantManagementContext" +import { ApprovalModalContent } from "./ApprovalModalContent" +import { checkAccess } from "@common/utils/permission" +import { useGlobalContext } from "@common/contexts/GlobalStateContext" + +export default function ManagementInsideService(){ + const {message, modal} = App.useApp() + const [serviceList, setServiceList] = useState([]) + const {fetchData} = useFetch() + const { setBreadcrumb} = useBreadcrumb() + const {teamId,appId} = useParams() + const navigateTo = useNavigate() + const [keyword, setKeyword] = useState('') + const { refreshGroup} = useOutletContext<{refreshGroup:()=>void,appName:string}>() + const {appName} = useTenantManagementContext() + const {accessData} = useGlobalContext() + + const onSearchWordChange = (e)=>{ + setKeyword(e.target.value) + } + + + const cancelSubscribeApply = (entity:TenantManagementServiceListItem) => { + return new Promise((resolve, reject)=>{ + fetchData>('application/subscription/cancel_apply',{method:'POST',eoParams:{subscription:entity.id!,application:appId!,team:teamId}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + const cancelSubscribe = (entity:TenantManagementServiceListItem) => { + return new Promise((resolve, reject)=>{ + fetchData>('application/subscription/cancel',{method:'POST',eoParams:{subscription:entity.id!,application:appId!,team:teamId}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + const openModal =async (type:'view'|'cancelSub'|'cancelSubApply',entity?:TenantManagementServiceListItem)=>{ + let title:string = '' + let content:string|React.ReactNode = '' + switch (type){ + case 'view':{ + message.loading('正在加载数据') + const {code,data,msg} = await fetchData>('app/subscription/approval',{method:'GET',eoParams:{subscription:entity!.id, app:appId,team:teamId},eoTransformKeys:['apply_project','apply_team','apply_time','approval_time']}) + message.destroy() + if(code === STATUS_CODE.SUCCESS){ + title='审批详情' + content = ; + }else{ + message.error(msg || '操作失败') + return + } + break; + } + case 'cancelSub': + title='取消订阅' + content='请确认是否取消订阅?' + break; + case 'cancelSubApply': + title='取消订阅申请' + content='请确认是否取消订阅申请?' + break; + } + + modal.confirm({ + title, + content, + onOk:()=>{ + switch (type){ + case 'view': + return true + case 'cancelSubApply': + return cancelSubscribeApply(entity!).then(res=>{if(res){getServiceList(); refreshGroup?.()}} ) + case 'cancelSub': + return cancelSubscribe(entity!).then(res=>{if(res){getServiceList(); refreshGroup?.()}} ) + } + }, + width:600, + okText:'确认', + okButtonProps:{ + disabled : !checkAccess( `team.application.authorization.${type}`, accessData) + }, + cancelText:'取消', + closable:true, + icon:<>, + }) + } + + + const dropdownMenu = (entity:TenantManagementServiceListItem) => [ + // { + // key: 'edit', + // label: ( + // // + // + // // + // ), + // }, + entity.applyStatus === 1 ? { + key: 'cancelSubApply', + label: ( + // + + // + ), + }:{ + key: 'cancelSub', + label: ( + // + + // + ), + }, + ] + + const getServiceList = ()=>{ + fetchData>('application/subscriptions',{method:'GET', eoParams:{application:appId,team:teamId},eoTransformKeys:['apply_status']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setServiceList(data.subscriptions && data.subscriptions.length > 0 ? [...data.subscriptions] : []) + // return {data:data.services, success: true,total:data.total} + }else{ + message.error(msg || '操作失败') + // return {data:[], success:false} + } + }) + } + + + useEffect(() => { + getServiceList() + }, []); + + return (
+
+ 服务 + debounce(onSearchWordChange, 100)(e) : undefined } onPressEnter={()=>getServiceList()} allowClear placeholder='搜索服务' prefix={{getServiceList()}}/>}/> +
+ { (keyword ? serviceList.filter(x=>x.service.name.includes(keyword)) :serviceList)?.length > 0 ? + x.service.name.includes(keyword)) :serviceList} + totalCount={(keyword ? serviceList.filter(x=>x.service.name.includes(keyword)) :serviceList).length} + itemContent={(index) => { + const item = (keyword ? serviceList.filter(x=>x.service.name.includes(keyword)) :serviceList)[index]; + return ( +
{item.service.name}{ item.applyStatus === 1 && + 审批中 + } +
+ + +
+
+ ); + }} + components={{ + List: forwardRef(({ style, children, ...props }, ref) => ( +
+ {children} +
+ )), + Item: ({ children, ...props }) => ( + <> + {children} + ) + }} + />:
} +
) +} \ No newline at end of file diff --git a/frontend/packages/market/src/pages/serviceHub/management/ServiceHubManagement.tsx b/frontend/packages/market/src/pages/serviceHub/management/ServiceHubManagement.tsx new file mode 100644 index 00000000..66a1f3de --- /dev/null +++ b/frontend/packages/market/src/pages/serviceHub/management/ServiceHubManagement.tsx @@ -0,0 +1,214 @@ +import { MenuProps, Menu, App, Avatar, Card, Tooltip, Empty } from "antd"; +import { useState, forwardRef, useEffect, useRef } from "react"; +import { VirtuosoGrid } from "react-virtuoso"; +import { BasicResponse, STATUS_CODE } from "@common/const/const"; +import { ServiceHubAppListItem } from "../../../const/serviceHub/type"; +import { useFetch } from "@common/hooks/http"; +import { useBreadcrumb } from "@common/contexts/BreadcrumbContext"; +import ManagementConfig, { ManagementConfigHandle } from "./ManagementConfig"; +import { useNavigate, useParams } from "react-router-dom"; +import { RouterParams } from "@core/components/aoplatform/RenderRoutes"; +import { SimpleTeamItem } from "@common/const/type"; +import { useTenantManagementContext } from "../../../contexts/TenantManagementContext"; +import { Icon } from "@iconify/react/dist/iconify.js"; +import { useGlobalContext } from "@common/contexts/GlobalStateContext"; + +export default function ServiceHubManagement() { + const { message ,modal} = App.useApp() + const { teamId} = useParams() + const [serviceList, setServiceList] = useState([]) + const {fetchData} = useFetch() + const { setBreadcrumb} = useBreadcrumb() + const addManagementRef = useRef(null) + const [pageLoading, setPageLoading] = useState(false) + const [serviceLoading, setServiceLoading] = useState(false) + const [teamList, setTeamList] = useState([]) + const {setAppName} = useTenantManagementContext() + const navigateTo = useNavigate() + const {getTeamAccessData,cleanTeamAccessData} = useGlobalContext() + type MenuItem = Required['items'][number]; + + +const getServiceList = ()=>{ + //console.log(pagination,sorter,categoryId,tagId) + setServiceLoading(true) + fetchData>('my_apps',{method:'GET', eoParams:{ team:teamId,keyword:''},eoTransformKeys:['api_num','subscribe_num','subscribe_verify_num']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setServiceList([...data.apps,{type:'addNewItem'}]) + }else{ + message.error(msg || '操作失败') + } + }).finally(()=>{ + setServiceLoading(false) + }) +} + + const onClick: MenuProps['onClick'] = (e) => { + navigateTo(`/tenantManagement/list/${e.key}`) + }; + + + const getTeamsList = ()=>{ + setPageLoading(true) + fetchData>('simple/teams/mine',{method:'GET',eoTransformKeys:['app_num','subscribe_num']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setTeamList(data.teams.map((x:SimpleTeamItem)=>({label:
{x.name}{x.appNum || 0}
, key:x.id}))) + if(!teamId && data.teams?.[0]?.id){ + navigateTo(data.teams[0].id) + } + }else{ + message.error(msg || '操作失败') + } + }).finally(()=>{ + setPageLoading(false) + }) +} + + + const openModal = async (type:'add'|'edit'|'delete')=>{ + + let title:string = '' + let content:string|React.ReactNode = '' + switch (type){ + case 'add': + title='添加应用' + content= + break; + // case 'edit':{ + // title='配置 Open Api' + // message.loading('正在加载数据') + // const {code,data,msg} = await fetchData>('external-app',{method:'GET',eoParams:{id:entity!.id}}) + // message.destroy() + // if(code === STATUS_CODE.SUCCESS){ + // content= + // }else{ + // message.error(msg || '操作失败') + // return + // } + // break;} + // case 'delete': + // title='删除' + // content='该数据删除后将无法找回,请确认是否删除?' + // break; + } + + modal.confirm({ + title, + content, + onOk:()=> { + switch (type){ + case 'add': + return addManagementRef.current?.save().then((res)=>{if(res === true) getTeamsList();getServiceList()}) + // case 'edit': + // return editManagementRef.current?.save().then((res)=>{if(res === true) manualReloadTable()}) + // case 'delete': + // return deleteManagement(entity!).then((res)=>{if(res === true) manualReloadTable()}) + } + }, + width:600, + okText:'确认', + cancelText:'取消', + closable:true, + icon:<>, + }) +} + +useEffect(()=>{ + if(teamId ){ + getTeamAccessData(teamId) + getServiceList() + } + return ()=>{ + cleanTeamAccessData() + } +},[teamId]) + +useEffect(() => { + setBreadcrumb( + [ + {title:'应用'} + ] + ) + getTeamsList() + setAppName('') +}, []); + + return (<>{ + teamList && teamList.length > 0 ? +
+
+
团队
+ +
+
+
应用
+ { + const item = serviceList[index]; + return ( +
{ + item.type === 'addNewItem' ?{openModal('add')}}> +
添加应用
+
: {setAppName(item.name);navigateTo(`/tenantManagement/${teamId}/inside/${item.id}/service`)}}> + {item.description || '暂无服务描述'} + + }
+ ); + }} + components={{ + List: forwardRef(({ style, children, ...props }, ref) => ( +
+ {children} +
+ )), + Item: ({ children, ...props }) => ( + <> + {children} + ) + }} + /> +
+
: + + } + ) +} + +const CardTitle = (service:ServiceHubAppListItem)=>{ + return( +
+ } /> +
+

{service.name}

+
+ + {(service.subscribeNum + service.subscribeVerifyNum)?? '-'} + +
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/packages/market/src/vite-env.d.ts b/frontend/packages/market/src/vite-env.d.ts new file mode 100644 index 00000000..11f02fe2 --- /dev/null +++ b/frontend/packages/market/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/frontend/packages/market/tsconfig.json b/frontend/packages/market/tsconfig.json new file mode 100644 index 00000000..61db1eac --- /dev/null +++ b/frontend/packages/market/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + /* Linting */ + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "paths": { + "@common/*": ["../common/src/*"], + "@core/*": ["../core/src/*"], + "@market/*": ["./src/*"] + }, + }, + "include": ["src", "public/iconpark_eolink.js", "public/iconpark_apinto.js", "../common/src/component/aoplatform/EditableTableWithModal.tsx", "../common/src/components/aoplatform/TransferTable.tsx", "../common/src/components/aoplatform/TreeWithMore.tsx", "../common/src/components/aoplatform/DatePicker.tsx", "../common/src/components/aoplatform/TimeRangeSelector.tsx", "../common/src/components/aoplatform/TimePicker.tsx", "../common/src/components/aoplatform/MemberTransfer.tsx", "../common/src/components/aoplatform/Navigation.tsx", "../common/src/components/aoplatform/PageList.tsx", "../common/src/components/aoplatform/GroupTree.tsx", "../common/src/components/aoplatform/ErrorBoundary.tsx", "../core/src/pages/serviceCategory/ServiceHubCategoryConfig.tsx"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/packages/market/tsconfig.node.json b/frontend/packages/market/tsconfig.node.json new file mode 100644 index 00000000..42872c59 --- /dev/null +++ b/frontend/packages/market/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/packages/market/vite.config.ts b/frontend/packages/market/vite.config.ts new file mode 100644 index 00000000..a1c1546f --- /dev/null +++ b/frontend/packages/market/vite.config.ts @@ -0,0 +1,52 @@ + +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' +import dynamicImportVars from '@rollup/plugin-dynamic-import-vars'; + +export default defineConfig({ + build:{ + outDir:'../../tenant_dist', + sourcemap: false, + chunkSizeWarningLimit: 50000, + rollupOptions: { + output: { + manualChunks(id) { + if (id.includes('node_modules')) { + return id.toString().split('node_modules/')[1].split('/')[0].toString(); + } + } + }, + } + }, + plugins: [react(), + dynamicImportVars({ + include:["src"], + exclude:[], + warnOnError:false + }), + ], + resolve: { + alias: [ + { find: /^~/, replacement: '' }, + { find: '@market', replacement: path.resolve(__dirname, './src') }, + { find: '@common', replacement: path.resolve(__dirname, '../common/src') }, + { find: '@core', replacement: path.resolve(__dirname, '../core/src') }, + ] + }, + server: { + proxy: { + '/api/v1': { + // target: 'http://uat.apikit.com:11204/mockApi/aoplatform/', + target: 'http://172.18.166.219:8488/', + changeOrigin: true, + }, + '/api2/v1': { + // target: 'http://uat.apikit.com:11204/mockApi/aoplatform/', + target: 'http://172.18.166.219:8488/', + changeOrigin: true, + } + } + }, + logLevel:'info' +}) diff --git a/frontend/packages/openApi/README.md b/frontend/packages/openApi/README.md new file mode 100644 index 00000000..69371903 --- /dev/null +++ b/frontend/packages/openApi/README.md @@ -0,0 +1,11 @@ +# `common` + +> TODO: description + +## Usage + +``` +const common = require('common'); + +// TODO: DEMONSTRATE API +``` diff --git a/frontend/packages/openApi/__tests__/openApi.test.js b/frontend/packages/openApi/__tests__/openApi.test.js new file mode 100644 index 00000000..61028bb1 --- /dev/null +++ b/frontend/packages/openApi/__tests__/openApi.test.js @@ -0,0 +1,7 @@ +'use strict'; + +const openApi = require('..'); +const assert = require('assert').strict; + +assert.strictEqual(openApi(), 'Hello from openApi'); +console.info('openApi tests passed'); diff --git a/frontend/packages/openApi/package.json b/frontend/packages/openApi/package.json new file mode 100644 index 00000000..3bb59abd --- /dev/null +++ b/frontend/packages/openApi/package.json @@ -0,0 +1,15 @@ +{ + "name": "open-api", + "version": "0.0.0", + "description": "openApi module", + "scripts": { + "dev": "vite", + "build": "vite build", + "test": "node ./__tests__/common.test.js" + }, + "dependencies": { + "copy-to-clipboard": "^3.3.3" + }, + "devDependencies": { + } +} diff --git a/frontend/packages/openApi/postcss.config.js b/frontend/packages/openApi/postcss.config.js new file mode 100644 index 00000000..80393090 --- /dev/null +++ b/frontend/packages/openApi/postcss.config.js @@ -0,0 +1,15 @@ +/* + * @Date: 2023-11-27 17:31:54 + * @LastEditors: maggieyyy + * @LastEditTime: 2023-11-29 15:49:05 + * @FilePath: \applatform\frontend\packages\core\postcss.config.js + */ +export default { + plugins: { + 'postcss-import': {}, + 'tailwindcss/nesting': {}, + tailwindcss: {}, + autoprefixer: {} + }, + } + \ No newline at end of file diff --git a/frontend/packages/openApi/src/pages/OpenApiConfig.tsx b/frontend/packages/openApi/src/pages/OpenApiConfig.tsx new file mode 100644 index 00000000..e8afd6fe --- /dev/null +++ b/frontend/packages/openApi/src/pages/OpenApiConfig.tsx @@ -0,0 +1,97 @@ + +import {App, Form, Input} from "antd"; +import {forwardRef, useEffect, useImperativeHandle} from "react"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {useFetch} from "@common/hooks/http.ts"; +import WithPermission from "@common/components/aoplatform/WithPermission.tsx"; +import {v4 as uuidv4} from 'uuid' + +export type OpenApiConfigFieldType = { + id?:string + name:string + desc:string +}; + +type OpenApiConfigProps = { + type:'add'|'edit' + entity?:OpenApiConfigFieldType +} + +export type OpenApiConfigHandle = { + save:()=>Promise +} + + +export const OpenApiConfig = forwardRef((props, ref) => { + const { message } = App.useApp() + const {type,entity} = props + const [form] = Form.useForm(); + const {fetchData} = useFetch() + const save:()=>Promise = ()=>{ + return new Promise((resolve, reject)=>{ + form.validateFields().then((value)=>{ + fetchData>('external-app',{method:type === 'add'? 'POST' : 'PUT',eoBody:(value), eoParams:type === 'add' ? {}:{id:entity!.id}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + useImperativeHandle(ref, ()=>({ + save + }) + ) + + useEffect(() => { + if(type === 'edit' && entity){ + form.setFieldsValue(entity) + }else{ + form.setFieldValue('id',uuidv4()) + } + }, []); + + return ( +
+ + label="应用名称" + name="name" + rules={[{ required: true, message: '必填项',whitespace:true }]} + > + + + + + label="应用 ID" + name="id" + rules={[{ required: true, message: '必填项' ,whitespace:true }]} + > + + + + + + + + +
) +}) \ No newline at end of file diff --git a/frontend/packages/openApi/src/pages/OpenApiList.tsx b/frontend/packages/openApi/src/pages/OpenApiList.tsx new file mode 100644 index 00000000..d8d66246 --- /dev/null +++ b/frontend/packages/openApi/src/pages/OpenApiList.tsx @@ -0,0 +1,284 @@ +import PageList from "@common/components/aoplatform/PageList.tsx"; +import {useEffect, useRef, useState} from "react"; +import {ActionType, ProColumns} from "@ant-design/pro-components"; +import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; +import { App, Divider, Switch} from "antd"; +import copy from "copy-to-clipboard"; +import {useFetch} from "@common/hooks/http.ts"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import {OpenApiConfig, OpenApiConfigFieldType, OpenApiConfigHandle} from "./OpenApiConfig.tsx"; +import { EntityItem } from "@common/const/type.ts"; +import { SimpleMemberItem } from "@common/const/type.ts"; +import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx"; +import { frontendTimeSorter } from "@common/utils/dataTransfer.ts"; + +type OpenApiTableListItem = { + id:string; + name: string; + token:string; + tags:string; + status:boolean; + operator:EntityItem; + updateTime:string; +}; + +const OPENAPI_LIST_COLUMNS: ProColumns[] = [ + { + title: '应用名称', + dataIndex: 'name', + ellipsis:true, + width:160, + fixed:'left' + }, + { + title: '应用 ID', + dataIndex: 'id', + ellipsis:true, + width: 140, + }, + { + title: '鉴权 Token', + dataIndex: 'token', + ellipsis:{ + showTitle:true + } + }, + { + title: '关联标签', + dataIndex: 'tag' + }, + { + title: '启用', + dataIndex: 'status' + }, + { + title: '更新者', + dataIndex: ['operator','name'], + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true + }, + { + title: '更新时间', + width:182, + dataIndex: 'updateTime', + sorter: (a,b)=>(new Date(a.updateTime)).getTime() - (new Date(b.updateTime)).getTime() + } +]; + + +export default function OpenApiList(){ + const { modal,message } = App.useApp() + // const [confirmLoading, setConfirmLoading] = useState(false); + const [init, setInit] = useState(true) + const [tableListDataSource, setTableListDataSource] = useState([]); + const [tableHttpReload, setTableHttpReload] = useState(true); + const [columns,setColumns] = useState[] >([]) + const pageListRef = useRef(null); + const addOpenApiRef = useRef(null) + const editOpenApiRef = useRef(null) + const {fetchData} = useFetch() + const { setBreadcrumb } = useBreadcrumb() + const [memberValueEnum, setMemberValueEnum] = useState<{[k:string]:{text:string}}>({}) + + const operation:ProColumns[] =[ + { + title: '操作', + key: 'option', + width: 266, + valueType: 'option', + fixed:'right', + render: (_: React.ReactNode, entity: OpenApiTableListItem) => [ + {refreshToken(entity)}} btnTitle="更新token"/>, + , + {copyToken(entity)}} btnTitle="复制token"/>, + , + {openModal('edit',entity)}} btnTitle="编辑"/>, + , + {openModal('delete',entity)}} btnTitle="删除"/> + ], + } + ] + + const getOpenApiList =(): Promise<{ data: OpenApiTableListItem[], success: boolean }>=> { + if(!tableHttpReload){ + setTableHttpReload(true) + return Promise.resolve({ + data: tableListDataSource, + success: true, + }); + } + return fetchData>('external-apps',{method:'GET',eoTransformKeys:['update_time']}).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + setTableListDataSource(data.apps) + setInit((prev)=>prev ? false : prev) + tableHttpReload && data.apps.sort((a:OpenApiTableListItem,b:OpenApiTableListItem)=>frontendTimeSorter(a,b,'updateTime')) + setTableHttpReload(false) + return {data:data.apps, success: true} + }else{ + message.error(msg || '操作失败') + return {data:[], success:false} + } + }).catch(() => { + return {data:[], success:false} + }) + } + + const refreshToken = (entity: OpenApiTableListItem)=>{ + fetchData>('external-app/token',{method:'PUT',eoParams:{id:entity.id}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + manualReloadTable() + }else{ + message.error(msg || '操作失败') + } + }) + } + + const copyToken = (entity: OpenApiTableListItem)=>{ + if(copy(entity.token)){ + message.success('复制成功') + }else{ + message.error('复制失败,请重试') + } + } + + const manualReloadTable = () => { + setTableHttpReload(true); // 表格数据需要从后端接口获取 + pageListRef.current?.reload() + }; + + const deleteOpenApi = (entity:OpenApiTableListItem)=>{ + return new Promise((resolve, reject)=>{ + fetchData>('external-app',{method:'DELETE',eoParams:{id:entity!.id}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + resolve(true) + }else{ + message.error(msg || '操作失败') + reject(msg || '操作失败') + } + }).catch((errorInfo)=> reject(errorInfo)) + }) + } + + const openModal = async (type:'add'|'edit'|'delete',entity?:OpenApiTableListItem)=>{ + + let title:string = '' + let content:string|React.ReactNode = '' + switch (type){ + case 'add': + title='添加 Open Api' + content= + break; + case 'edit':{ + title='配置 Open Api' + message.loading('正在加载数据') + const {code,data,msg} = await fetchData>('external-app',{method:'GET',eoParams:{id:entity!.id}}) + message.destroy() + if(code === STATUS_CODE.SUCCESS){ + content= + }else{ + message.error(msg || '操作失败') + return + } + break;} + case 'delete': + title='删除' + content='该数据删除后将无法找回,请确认是否删除?' + break; + } + + modal.confirm({ + title, + content, + onOk:()=> { + switch (type){ + case 'add': + return addOpenApiRef.current?.save().then((res)=>{if(res === true) manualReloadTable()}) + case 'edit': + return editOpenApiRef.current?.save().then((res)=>{if(res === true) manualReloadTable()}) + case 'delete': + return deleteOpenApi(entity!).then((res)=>{if(res === true) manualReloadTable()}) + } + }, + width:600, + okText:'确认', + cancelText:'取消', + closable:true, + icon:<>, + }) + } + + const changeOpenApiStatus = (enabled:boolean,entity:OpenApiTableListItem)=>{ + fetchData>(`external-app/${enabled ? 'disable' :'enable'}`,{method:'PUT',eoParams:{id:entity.id}}).then(response=>{ + const {code,msg} = response + if(code === STATUS_CODE.SUCCESS){ + message.success(msg || '操作成功!') + manualReloadTable() + }else{ + message.error(msg || '操作失败') + } + }) + } + + + const getMemberList = async ()=>{ + setMemberValueEnum({}) + const {code,data,msg} = await fetchData>('simple/member',{method:'GET'}) + if(code === STATUS_CODE.SUCCESS){ + const tmpValueEnum:{[k:string]:{text:string}} = {} + data.members?.forEach((x:SimpleMemberItem)=>{ + tmpValueEnum[x.name] = {text:x.name} + }) + setMemberValueEnum(tmpValueEnum) + }else{ + message.error(msg || '操作失败') + } + } + + + useEffect(() => { + setBreadcrumb([{ title: 'Open Api'}]) + getMemberList() + setColumns(OPENAPI_LIST_COLUMNS + .map((x)=>{ + if(x.dataIndex === 'status' ){ + x.render = (_,record)=>( +
{e?.stopPropagation()}}>{ changeOpenApiStatus(e,record)}} />
+ ) + } + if(x.filters &&((x.dataIndex as string[])?.indexOf('updater') !== -1 )){ + x.valueEnum = memberValueEnum + } + return x + } + ) + ) + }, []); + + return ( getOpenApiList()} + dataSource={tableListDataSource} + showPagination={false} + primaryKey="id" + addNewBtnTitle="添加应用" + addNewBtnAccess="system.openapi.self.add" + onChange={() => { + setTableHttpReload(false) + }} + onAddNewBtnClick={() => { + openModal('add') + }} + onRowClick={(row:OpenApiTableListItem)=>openModal('edit',row)} + tableClickAccess="system.openapi.self.edit" + />) + +} \ No newline at end of file diff --git a/frontend/packages/openApi/tailwind.config.js b/frontend/packages/openApi/tailwind.config.js new file mode 100644 index 00000000..e53065a4 --- /dev/null +++ b/frontend/packages/openApi/tailwind.config.js @@ -0,0 +1,86 @@ +/* + * @Date: 2023-11-27 17:31:44 + * @LastEditors: maggieyyy + * @LastEditTime: 2024-06-05 10:36:11 + * @FilePath: \frontend\packages\market\tailwind.config.js + */ +/** @type {import('tailwindcss').Config} */ + +export default { + important:true, + content: [ + `./index.html`, + `../*/src/**/*.{js,ts,jsx,tsx}`, + ], + theme: { + extend: { + width: { + INPUT_NORMAL: '100%', + // INPUT_NORMAL: '346px', + INPUT_LARGE: '508px', + GROUP: '240px', + SEARCH: '276px', + LOG: '254px' + }, + minHeight:{ + TEXTAREA:'68px' + }, + borderRadius: { + DEFAULT: 'var(--border-radius)', + SEARCH_RADIUS: '50px' + }, + boxShadow:{ + SCROLL: '0 2px 2px #0000000d', + SCROLL_TOP:' 0 -2px 2px -2px var(--border-color)' + }, + colors: { + DISABLE_BG: 'var(--disabled-background-color)', + MAIN_TEXT: 'var(--text-color)', + MAIN_HOVER_TEXT: 'var(--text-hover-color)', + SECOND_TEXT:'var(--disabled-text-color)', + MAIN_BG: 'var(--background-color)', + MENU_BG:'var(--MENU-BG-COLOR)', + 'bar-theme': 'var(--bar-background-color)', + BORDER: 'var(--border-color)', + NAVBAR_BTN_BG: 'var(--item-active-background-color)', + MAIN_DISABLED_BG: 'var(--disabled-background-color)', + theme: 'var(--primary-color)', + DESC_TEXT: 'var(--TITLE_TEXT)', + HOVER_BG: 'var(--item-hover-background-color)', + guide_cluster: '#ee6760', + guide_upstream: '#f9a429', + guide_api: '#71d24d', + guide_publishApi: '#5884ff', + guide_final: '#915bf9', + table_text: 'var(--table-text-color)', + status_success:'#138913', + status_fail:"#ff3b30", + status_update:"#03a9f4", + status_pending:"#ffa500", + status_offline:"#8f8e93", + A_HOVER:'var(--button-primary-hover-background-color)' + }, + spacing: { + mbase: 'var(--FORM_SPAN)', + label: '12px', // 选择器和label之间的间距,待删 + btnbase: 'var(--LAYOUT_MARGIN)', // x方向的间距 + btnybase: 'var(--LAYOUT_MARGIN)', // y轴方向的间距 + btnrbase: '20px', // 页面最右侧边距20px + formtop: 'var(--FORM_SPAN)', + icon: '5px', + blockbase: '40px', + DEFAULT_BORDER_RADIUS: 'var(--border-radius)', + TREE_TITLE:'var(--small-padding) var(--LAYOUT_PADDING);' + }, + borderColor: { + 'color-base': 'var(--border-color)' + } + } + }, + plugins: [], + corePlugins: { + preflight: false, + }, + } + + \ No newline at end of file diff --git a/frontend/packages/openApi/tsconfig.json b/frontend/packages/openApi/tsconfig.json new file mode 100644 index 00000000..61db1eac --- /dev/null +++ b/frontend/packages/openApi/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + /* Linting */ + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "paths": { + "@common/*": ["../common/src/*"], + "@core/*": ["../core/src/*"], + "@market/*": ["./src/*"] + }, + }, + "include": ["src", "public/iconpark_eolink.js", "public/iconpark_apinto.js", "../common/src/component/aoplatform/EditableTableWithModal.tsx", "../common/src/components/aoplatform/TransferTable.tsx", "../common/src/components/aoplatform/TreeWithMore.tsx", "../common/src/components/aoplatform/DatePicker.tsx", "../common/src/components/aoplatform/TimeRangeSelector.tsx", "../common/src/components/aoplatform/TimePicker.tsx", "../common/src/components/aoplatform/MemberTransfer.tsx", "../common/src/components/aoplatform/Navigation.tsx", "../common/src/components/aoplatform/PageList.tsx", "../common/src/components/aoplatform/GroupTree.tsx", "../common/src/components/aoplatform/ErrorBoundary.tsx", "../core/src/pages/serviceCategory/ServiceHubCategoryConfig.tsx"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/packages/openApi/tsconfig.node.json b/frontend/packages/openApi/tsconfig.node.json new file mode 100644 index 00000000..42872c59 --- /dev/null +++ b/frontend/packages/openApi/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/packages/systemRunning/README.md b/frontend/packages/systemRunning/README.md new file mode 100644 index 00000000..631a11fd --- /dev/null +++ b/frontend/packages/systemRunning/README.md @@ -0,0 +1,11 @@ +# `systemRunning` + +> TODO: description + +## Usage + +``` +const systemRunning = require('systemRunning'); + +// TODO: DEMONSTRATE API +``` diff --git a/frontend/packages/systemRunning/__tests__/systemRunning.test.js b/frontend/packages/systemRunning/__tests__/systemRunning.test.js new file mode 100644 index 00000000..3d25b66f --- /dev/null +++ b/frontend/packages/systemRunning/__tests__/systemRunning.test.js @@ -0,0 +1,7 @@ +'use strict'; + +const systemRunning = require('..'); +const assert = require('assert').strict; + +assert.strictEqual(systemRunning(), 'Hello from systemRunning'); +console.info('systemRunning tests passed'); diff --git a/frontend/packages/systemRunning/package.json b/frontend/packages/systemRunning/package.json new file mode 100644 index 00000000..0bc17818 --- /dev/null +++ b/frontend/packages/systemRunning/package.json @@ -0,0 +1,8 @@ +{ + "name": "systemrunning", + "version": "1.0.0", + "description": "> TODO: description", + "author": "maggieyyy <61950669+maggieyyy@users.noreply.github.com>", + "homepage": "", + "license": "ISC" +} diff --git a/frontend/packages/systemRunning/postcss.config.js b/frontend/packages/systemRunning/postcss.config.js new file mode 100644 index 00000000..80393090 --- /dev/null +++ b/frontend/packages/systemRunning/postcss.config.js @@ -0,0 +1,15 @@ +/* + * @Date: 2023-11-27 17:31:54 + * @LastEditors: maggieyyy + * @LastEditTime: 2023-11-29 15:49:05 + * @FilePath: \applatform\frontend\packages\core\postcss.config.js + */ +export default { + plugins: { + 'postcss-import': {}, + 'tailwindcss/nesting': {}, + tailwindcss: {}, + autoprefixer: {} + }, + } + \ No newline at end of file diff --git a/frontend/packages/systemRunning/src/pages/SystemRunning.tsx b/frontend/packages/systemRunning/src/pages/SystemRunning.tsx new file mode 100644 index 00000000..ed0be460 --- /dev/null +++ b/frontend/packages/systemRunning/src/pages/SystemRunning.tsx @@ -0,0 +1,605 @@ + +import { useRef, useState, useEffect, useCallback } from "react"; +import { useFetch } from "@common/hooks/http.ts"; +import { useParams } from "react-router-dom"; +import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx"; +import { App, Button, Spin, Tooltip } from "antd"; +import { debounce } from "lodash-es"; +import { LoadingOutlined, ZoomInOutlined, ZoomOutOutlined } from "@ant-design/icons"; +import G6, { Graph, registerEdge, Item } from "@antv/g6"; +import { PictureTypeEnum, NodeClickItem, GraphData } from "@core/const/system-running/type.ts"; +import { UnionFind, edgesFormatter, getNodeSpacing, nodesFormatter } from "@common/utils/systemRunning.ts"; +import { EDGE_STYLE, END_ARROW_STYLE, OUT_SPACE_CONTENT_EDGE_COLOR, RELATIVE_PICTURE_NODE_FONTSIZE, SELF_SPACE_CONTENT_EDGE_COLOR, SYSTEM_TUNNING_CONFIG } from "@core/const/system-running/const.ts"; +import ReactDOM from "react-dom"; +import {BasicResponse, STATUS_CODE} from "@common/const/const.ts"; +import SystemRunningInstruction from "./SystemRunningInstruction.tsx"; +import { useBreadcrumb } from "@common/contexts/BreadcrumbContext.tsx"; + +export type TopologyItem = { + projects:TopologyProjectItem[] + services:TopologyServiceItem[] +} + +export type TopologyProjectItem = { + id:string, + name:string, + invokeServices:string[], + clusters?:string + isApp?:boolean + isServer?:boolean +} + +export type TopologyServiceItem = { + id:string, + name:string, + project:string +} + +enum EdgeEvent { + Mouseenter = 'mouseenter', + Mouseleave = 'mouseleave' +} + +type nodeAny = unknown +const subjectColors = [ + '#5F95FF', // blue + '#61DDAA', + '#65789B', + '#F6BD16', + '#7262FD', + '#78D3F8', + '#9661BC', + '#F6903D', + '#008685', + '#F08BB4', +]; +const backColor = '#fff'; +const theme = 'default'; +const disableColor = '#777'; +const colorSets = G6.Util.getColorSetsBySubjectColors( + subjectColors, + backColor, + theme, + disableColor, +); + + +// cache the initial node and combo info +const itemMap :Record= {}; + +export default function SystemRunning(){ + const {message} = App.useApp() + const graphRef = useRef(null); + const graphContainerRef = useRef(null); + const {topologyId} = useParams() + const [graph, setGraph] = useState(null); + const [graphData, setGraphData] = useState(); + const [currentNode, setCurrentNode] = useState() + const [showEdgeTooltip, setShowEdgeTooltip] = useState(false) + const [edgeTooltipX, setEdgeTooltipX] = useState(0) + const [edgeTooltipY, setEdgeTooltipY] = useState(0) + const [edgeTooltipContent, setEdgeTooltipContent] = useState() + const [pictureType, setPictureType] = useState(PictureTypeEnum.Global) + const { fetchData } = useFetch() + const textColor:string = '#666' + const [showGraph, setShowGraph] = useState(false) + const { setBreadcrumb } = useBreadcrumb() + const [zoomNum, setZoomNum] = useState(1) + const [loading, setLoading ] = useState(true) + const [categories, setCategories] = useState(undefined) + + /** + * @description 关联关系转化器,将接口数据转为 g6 渲染需要的格式 + */ + const relativeFormatter = (data: TopologyItem) => { + const { projects, services } = data + const serviceMap:Map = new Map() + services.forEach((s:TopologyServiceItem)=>{ + serviceMap.set(s.id,s) + }) + // Map> + const tmpProjectConnectMap:Map> = new Map() + projects.forEach((p:TopologyProjectItem) => { + const invokedMap = new Map() + p.invokeServices?.forEach((s:string) => { + const invokedProject = serviceMap.get(s) + if(invokedProject){ + invokedMap.has(invokedProject.project) ? invokedMap.get(invokedProject.project)?.push(invokedProject) : invokedMap.set(invokedProject.project, [invokedProject]) + }else{ + console.warn('存在无所属系统的服务:', s) + } + }) + tmpProjectConnectMap.set(p.id, invokedMap) + }) + const newNodes = nodesFormatter(projects) + const newEdges = edgesFormatter(tmpProjectConnectMap) + + // 从 edges 中提取所有唯一的节点 + // const allNodeIds = tmpProjectConnectMap.map(({ source, target }) => source || []).concat(tmpProjectConnectMap.map(({ source, target }) => target || [])).filter(Boolean); + // const nodes: Node[] = allNodeIds.map(id => ({ id })); + // 从 edges 中提取所有唯一的节点,并将 Set 转换为数组 + const allNodeIds:string[] = Array.from(new Set(newEdges.flatMap(edge => [edge.source, edge.target]))) as string[]; + + // 初始化 UnionFind,并处理所有的边 + const unionFind = new UnionFind(allNodeIds); + newEdges.forEach(({ source, target }) => { + unionFind.union(source, target); + }); + + // 预设的颜色数组 + const colors: string[] = [ + '#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', + // ... 根据需要添加更多颜色 + ]; + + + // 使用 Union-Find 算法处理所有的边 + tmpProjectConnectMap.forEach(({ source, target }) => { + unionFind.union(source, target); + }); + + // 为每个连通分量分配颜色,并更新 nodes 数组 + const clusterToColor: Record = {}; + const categories: Record = {}; + const newCom = [] + + + newNodes.forEach(node => { + const root = unionFind.find(node.id); + categories[root] = categories[root] || []; + categories[root].push(node.id); + if (!clusterToColor[root]) { + // 分配颜色,确保同一连通分量的节点颜色相同 + clusterToColor[root] = colors[Math.max(0, Object.keys(clusterToColor).length) % colors.length]; + } + node.cluster = root || 'none'; + node.comboId = `${root || 'none'}-combo`; + // node.color = clusterToColor[root]; + }); + + + let i:number = 0 + + for(const c in categories){ + const color = colorSets[i % colorSets.length]; + const comboStyle = { + stroke: color.mainStroke, + fill: color.mainFill, + opacity: 0.8} + const comboId = `${c === 'undefined' ? 'none' : (c||'none') }-combo` + newCom.push(comboId === 'none-combo' ?{id:comboId,style:{ + fill:'transparent', + stroke:'transparent', + fillOpacity: 0, + strokeOpacity: 0, + active: { + // 设置激活状态下的透明度 + fill: 'transparent', + stroke: 'transparent', + fillOpacity: 0, + strokeOpacity: 0, + }, + inactive: { + fill: 'transparent', + stroke: 'transparent', + // 设置非激活状态下的透明度 + fillOpacity: 0, + strokeOpacity: 0, + }, + highlight: { + fill: 'transparent', + stroke: 'transparent', + // 设置高亮状态下的透明度 + fillOpacity: 0, + strokeOpacity: 0, + } + }}:{ + id:comboId, + style:comboStyle + }) + itemMap[comboId] = {style : { ...comboStyle }} + i++ + } + + + + newNodes.forEach(node => { + const parentCombo = itemMap[node.comboId]; + if(node.isApp){ + node.style = { + stroke: '#ffa940', + fill: '#ffa94033', + } + }else if (parentCombo) { + node.style = { + stroke: parentCombo.style.stroke, + fill: parentCombo.style.fill + } + } + // node.color = clusterToColor[root]; + }); + + + return { + nodes: newNodes, + edges: newEdges, + combos:newCom + } + } + + + + const getNodeData = ()=>{ + setLoading(true) + fetchData>('topology',{method:'GET',eoTransformKeys:['invoke_services','is_app','is_server']},).then(response=>{ + const {code,data,msg} = response + if(code === STATUS_CODE.SUCCESS){ + const newGraphData = relativeFormatter(data) + setGraphData(newGraphData) + setShowGraph(newGraphData?.nodes?.length > 0) + + }else{ + message.error(msg || '操作失败') + } + }).finally(()=>setLoading(false)) + } + + const handleWindowResize = useCallback(debounce(() => { + if (graphContainerRef.current && graphRef.current && !graphRef.current?.get('destroyed')) { + graphRef.current.changeSize( + graphContainerRef.current.offsetWidth, + graphContainerRef.current.offsetHeight, + ); + graphRef.current?.fitCenter() + // graphRef.current?.fitView() + } + }, 400), []); + + + /** + * @description 点击节点的回调 + */ + const clickNode = (item: NodeClickItem) => { + // console.log(item) + // router.navigate(['/', 'home', 'api-relative', item.id]) + } + + const updateSelected = ()=> { + if (!currentNode) return + // 设置节点状态 + graph?.setItemState(currentNode, 'selected', true) + } + + /** + * @description 更新缩放比例 + */ + const updateZoomTo = (increase:boolean) =>{ + const zoom:number = graph?.getZoom() || zoomNum + if((increase && zoom*10 >= 20 )||(!increase && zoom*10 <= 2)) return + setZoomNum(increase ?( zoom*10 + 2)/10 : (zoom*10 - 2)/10) + graph?.zoomTo(increase ? ( zoom*10 + 2)/10 : (zoom*10 - 2)/10) + } + + const initGraph = () => { + return new G6.Graph({ + container: ReactDOM.findDOMNode(graphContainerRef.current) as HTMLDivElement, + groupByTypes: false, + // plugins: [tooltip], + fitCenter:true, + // fitView:true, + layout: { + type: 'comboForce', + // 稳定系数,初始动画的加载时长(稳定性)=节点数量/稳定系数 + alphaDecay: 0.08, + // // 因为有分组的存在,整体布局需要往左偏移一点 + // // center: [(graphContainerRef.current?.scrollWidth || 300) / 2 - 150,( graphContainerRef.current?.scrollHeight || 0) / 2], + preventOverlap: true, + preventNodeOverlap:true, + preventComboOverlap:true, + // nodeCollideStrength:1, + // collideStrength:1, + comboCollideStrength:0.9, + nodeSize:24, + padding:[20,20,20,20], + // linkDistance: 30, + nodeStrength: -10, + edgeStrength: 0.1, + // nodeSpacing:40, + // comboSpacing:10, + comboPadding:30, + clustering:true, + clusterNodeStrength: 1000, + clusterEdgeDistance: 50, + clusterNodeSize: 100, + // clusterFociStrength: 1, + // charge: (d: nodeAny) => { + // return 100 + // }, + // linkStrength:()=>{ + // return 100 + // }, + // nodeSpacing: (d: nodeAny,v:nodeAny) => { + // console.log(d, v) + // if (d.comboId=== 'none-combo' || d.cluster==="none") { + // return 40 + // } + // return 50 + // }, + // onTick:()=>{console.log('ticking')}, + // onLayoutEnd:()=>{console.log('layout end')} + }, + modes: { + default: ['drag-combo','drag-canvas', 'drag-node', 'zoom-canvas','activate-relations' + ] + }, + defaultNode: { + size: [24, 24], + style: { + radius: 5, + stroke: '#69c0ff', + lineWidth: 1, + fillOpacity: 1 + }, + labelCfg: { + style: { + fontSize: RELATIVE_PICTURE_NODE_FONTSIZE, + fill: textColor + }, + position: 'bottom', + offset: 12 + } + }, + defaultEdge: { + // type: 'quadratic', + label: '调用服务', + labelCfg: { + style: { + fill: '5B8FF9', + opacity: 0 // 将透明度设置为0,隐藏提示信息,hover 才出现 + } + } + }, + defaultCombo: { + labelCfg: { + style: { + fill: '#666', // combo 的文本颜色 + }, + }, + }, + }) + } + + const updateEdgeLabel = (type: EdgeEvent, edge: Item) => { + + if (type === EdgeEvent.Mouseenter) { + // hover 边的时候出提示 + edge.update({ + labelCfg: { + style: { + opacity: 1, + fill: '#5B8FF9', + // @ts-expect-error g6 内部没定义好类型 + cursor: 'pointer', + } + } + }) + return + } + // 移出边时需要隐藏提示 + edge.update({ + labelCfg: { + style: { + opacity: 0, + // @ts-ignore g6 内部没定义好类型 + cursor: 'pointer' + } + } + }) + } + + const refreshDragNodePosition = (e: unknown)=> { + const model = e.item.get('model') + model.fx = e.x + model.fy = e.y + } + + const initGraphEvent = (graph:Graph, opts: { + onClickEdge?: (model: { target: string; source: string }) => void + onClickNode?: (item: NodeClickItem) => void + }) => { + + graph.on('node:mouseenter', (e) => { + const node = e.item + if (!node) return + // hover 出文本 + const element = node.getKeyShape() + element.attr('cursor', 'pointer') + }) + + graph.on('node:mouseleave', (e) => { + const node = e.item + if (!node) return + const element = node.getKeyShape() + element.attr('cursor', 'default') + }) + + // 目前只找到这种性能较低的方法 + graph.edge((edge) => { + const sourceNode = graph.findById(edge.source as string) + const theme = sourceNode?._cfg?.model?.isSelfSpace ? SELF_SPACE_CONTENT_EDGE_COLOR : OUT_SPACE_CONTENT_EDGE_COLOR + return { + id: edge.id, + ...EDGE_STYLE, + style: { + stroke: theme, + endArrow: { + ...END_ARROW_STYLE, + fill: theme + } + } + } + }) + + graph.on('edge:mouseenter', (evt) => { + if(evt.item){ + graph.setItemState(evt.item, 'running', true) + } + const edge = evt.item + if (edge) { + updateEdgeLabel(EdgeEvent.Mouseenter, edge) + const model = edge.getModel() + const { endPoint,startPoint } = model + // y=endPoint.y - height / 2,在同一水平线上,x值=endPoint.x - width - 10 + const y = (endPoint.y + startPoint.y) /2 + const x = (endPoint.x + startPoint.x )/2 + const point = graph.getCanvasByPoint(x, y) + setEdgeTooltipX(point.x + 194) // 加上页面左侧导航菜单宽度 + setEdgeTooltipY(point.y + 50) // 加上页面顶部导航与按钮高度 + setShowEdgeTooltip(true) + setEdgeTooltipContent(model?._projectInfo) + } + }) + + graph.on('edge:mouseleave', (evt) => { + const { item } = evt + if (item) { + graph.clearItemStates(item, ['running']) + updateEdgeLabel(EdgeEvent.Mouseleave, item) + } + setEdgeTooltipContent(undefined) + setShowEdgeTooltip(false) + }) + } + + const getGraph = ( + opts: { + onClickEdge?: (model: { target: string; source: string }) => void + onClickNode?: (item: NodeClickItem) => void + } + ) => { + const graph = initGraph() + graph.setMaxZoom(3) + graph.setMinZoom(0.2) + initGraphEvent(graph, opts) + return graph + } + + useEffect(()=>{ + if(topologyId !== undefined){ + setPictureType(PictureTypeEnum.Part) + setCurrentNode(topologyId) + return + } + setPictureType(PictureTypeEnum.Global) + },[topologyId]) + + useEffect(() => { + if (graphContainerRef.current) { + registerEdge('line-running',SYSTEM_TUNNING_CONFIG,'quadratic') + const graph = getGraph({ + onClickNode: (item: NodeClickItem) => { + clickNode(item) + }}) + + graph.on('beforelayout', async () => { + updateSelected() + }) + + setGraph(graph) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + graphRef.current = graph; + + // 添加窗口大小变化的监听器 + window.addEventListener('resize', handleWindowResize); + + // 组件卸载时清理资源 + return () => { + window.removeEventListener('resize', handleWindowResize); + if (graphRef.current) { + graphRef.current.destroy(); + } + }; + } + }, [handleWindowResize,showGraph,loading]); + + useEffect(()=>{ + if (!graph || !graphData || !showGraph || loading) return; + graph.clear() + graph.data(graphData) + + setTimeout(()=>{ + const { nodes } = graphData as GraphData + + if (nodes?.length) { + graph.updateLayout({ + nodeSpacing: getNodeSpacing(nodes.length,nodes), + comboSpacing: getNodeSpacing(nodes.length) + }) + } + + graph.render() + },200) + },[graph,graphData,showGraph,loading]) + + + useEffect(() => { + setBreadcrumb([{title:'系统拓扑图'}]) + getNodeData() + }, []); + + return (<> + { + showGraph ? +
+
+
+
+
+
+
+
+
+
+ + +
+
+ :} spinning={loading}>{!loading && } + }) + } + + const EdgeToolTips = ({ x, y, content}) => { + return ( + //
+ //
+ //
+ //
Edge
+ //
+ //
+ { + content.map((x:TopologyServiceItem)=>{x?.name || ''}) + }} + placement='bottomLeft' + color="#fff" + key="edge-tooltip" + > +
+
+ ) + } + \ No newline at end of file diff --git a/frontend/packages/systemRunning/src/pages/SystemRunningInstruction.tsx b/frontend/packages/systemRunning/src/pages/SystemRunningInstruction.tsx new file mode 100644 index 00000000..4eb94599 --- /dev/null +++ b/frontend/packages/systemRunning/src/pages/SystemRunningInstruction.tsx @@ -0,0 +1,29 @@ +import { useBreadcrumb } from "@common/contexts/BreadcrumbContext"; +import { useEffect } from "react"; +import { Link } from "react-router-dom"; +export default function SystemRunningInstruction() { + const { setBreadcrumb } = useBreadcrumb() + + useEffect(()=>{ + setBreadcrumb([ + {title:'系统拓扑图'} + ]) + },[]) + return ( +
+
+

系统配置并开启拓扑关联

+

系统拓扑功能辅助用户可视化了解系统结构,分析系统性能,规划系统部署,诊断系统故障。有助于提高系统可见性、可靠性和可维护性。

+ {/*

更多配置及关联问题,请点击帮助中心 + {/* 查看更多 * +

*/} +
+
+

内部数据服务设置

+

支持根据权限,拆分人员对 API 添加 、上游设置、鉴权设置等信息发布及管理;同时支持管理 API 调用服务(包含第三方调用)及订阅;

+

添加内部数据服务信息

+
+
+
+ ) +} \ No newline at end of file diff --git a/frontend/packages/systemRunning/tailwind.config.js b/frontend/packages/systemRunning/tailwind.config.js new file mode 100644 index 00000000..e53065a4 --- /dev/null +++ b/frontend/packages/systemRunning/tailwind.config.js @@ -0,0 +1,86 @@ +/* + * @Date: 2023-11-27 17:31:44 + * @LastEditors: maggieyyy + * @LastEditTime: 2024-06-05 10:36:11 + * @FilePath: \frontend\packages\market\tailwind.config.js + */ +/** @type {import('tailwindcss').Config} */ + +export default { + important:true, + content: [ + `./index.html`, + `../*/src/**/*.{js,ts,jsx,tsx}`, + ], + theme: { + extend: { + width: { + INPUT_NORMAL: '100%', + // INPUT_NORMAL: '346px', + INPUT_LARGE: '508px', + GROUP: '240px', + SEARCH: '276px', + LOG: '254px' + }, + minHeight:{ + TEXTAREA:'68px' + }, + borderRadius: { + DEFAULT: 'var(--border-radius)', + SEARCH_RADIUS: '50px' + }, + boxShadow:{ + SCROLL: '0 2px 2px #0000000d', + SCROLL_TOP:' 0 -2px 2px -2px var(--border-color)' + }, + colors: { + DISABLE_BG: 'var(--disabled-background-color)', + MAIN_TEXT: 'var(--text-color)', + MAIN_HOVER_TEXT: 'var(--text-hover-color)', + SECOND_TEXT:'var(--disabled-text-color)', + MAIN_BG: 'var(--background-color)', + MENU_BG:'var(--MENU-BG-COLOR)', + 'bar-theme': 'var(--bar-background-color)', + BORDER: 'var(--border-color)', + NAVBAR_BTN_BG: 'var(--item-active-background-color)', + MAIN_DISABLED_BG: 'var(--disabled-background-color)', + theme: 'var(--primary-color)', + DESC_TEXT: 'var(--TITLE_TEXT)', + HOVER_BG: 'var(--item-hover-background-color)', + guide_cluster: '#ee6760', + guide_upstream: '#f9a429', + guide_api: '#71d24d', + guide_publishApi: '#5884ff', + guide_final: '#915bf9', + table_text: 'var(--table-text-color)', + status_success:'#138913', + status_fail:"#ff3b30", + status_update:"#03a9f4", + status_pending:"#ffa500", + status_offline:"#8f8e93", + A_HOVER:'var(--button-primary-hover-background-color)' + }, + spacing: { + mbase: 'var(--FORM_SPAN)', + label: '12px', // 选择器和label之间的间距,待删 + btnbase: 'var(--LAYOUT_MARGIN)', // x方向的间距 + btnybase: 'var(--LAYOUT_MARGIN)', // y轴方向的间距 + btnrbase: '20px', // 页面最右侧边距20px + formtop: 'var(--FORM_SPAN)', + icon: '5px', + blockbase: '40px', + DEFAULT_BORDER_RADIUS: 'var(--border-radius)', + TREE_TITLE:'var(--small-padding) var(--LAYOUT_PADDING);' + }, + borderColor: { + 'color-base': 'var(--border-color)' + } + } + }, + plugins: [], + corePlugins: { + preflight: false, + }, + } + + \ No newline at end of file diff --git a/frontend/packages/systemRunning/tsconfig.json b/frontend/packages/systemRunning/tsconfig.json new file mode 100644 index 00000000..7c6fbbf2 --- /dev/null +++ b/frontend/packages/systemRunning/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + /* Linting */ + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noFallthroughCasesInSwitch": true, + "paths": { + "@common/*": ["../common/src/*"], + "@core/*": ["../core/src/*"], + }, + }, + "include": ["src", "public/iconpark_eolink.js", "public/iconpark_apinto.js", "../common/src/component/aoplatform/EditableTableWithModal.tsx", "../common/src/components/aoplatform/TransferTable.tsx", "../common/src/components/aoplatform/TreeWithMore.tsx", "../common/src/components/aoplatform/DatePicker.tsx", "../common/src/components/aoplatform/TimeRangeSelector.tsx", "../common/src/components/aoplatform/TimePicker.tsx", "../common/src/components/aoplatform/MemberTransfer.tsx", "../common/src/components/aoplatform/Navigation.tsx", "../common/src/components/aoplatform/PageList.tsx", "../common/src/components/aoplatform/GroupTree.tsx", "../common/src/components/aoplatform/ErrorBoundary.tsx", "../core/src/pages/serviceCategory/ServiceHubCategoryConfig.tsx"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/frontend/packages/systemRunning/tsconfig.node.json b/frontend/packages/systemRunning/tsconfig.node.json new file mode 100644 index 00000000..42872c59 --- /dev/null +++ b/frontend/packages/systemRunning/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/pnpm-workspace.yaml b/frontend/pnpm-workspace.yaml new file mode 100644 index 00000000..ccdc80cd --- /dev/null +++ b/frontend/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "packages/*" \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 00000000..acf9d2fe --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,88 @@ +/* + * @Date: 2023-11-27 17:31:44 + * @LastEditors: maggieyyy + * @LastEditTime: 2024-06-04 15:03:36 + * @FilePath: \frontend\tailwind.config.js + */ +/** @type {import('tailwindcss').Config} */ + +module.exports = { + important:true, + corePlugins:{ + preflight:false + }, + content: [ + `./packages/**/index.html`, + `./packages/**/src/**/*.{js,ts,jsx,tsx}`,], + theme: { + extend: { + width: { + INPUT_NORMAL: '100%', + // INPUT_NORMAL: '346px', + INPUT_LARGE: '508px', + GROUP: '240px', + SEARCH: '276px', + LOG: '254px' + }, + minHeight:{ + TEXTAREA:'68px' + }, + borderRadius: { + DEFAULT: 'var(--border-radius)', + SEARCH_RADIUS: '50px' + }, + boxShadow:{ + SCROLL: '0 2px 2px #0000000d', + SCROLL_TOP:' 0 -2px 2px -2px var(--border-color)' + }, + colors: { + DISABLE_BG: 'var(--disabled-background-color)', + MAIN_TEXT: 'var(--text-color)', + MAIN_HOVER_TEXT: 'var(--text-hover-color)', + SECOND_TEXT:'var(--disabled-text-color)', + MAIN_BG: 'var(--background-color)', + MENU_BG:'var(--MENU-BG-COLOR)', + 'bar-theme': 'var(--bar-background-color)', + BORDER: 'var(--border-color)', + NAVBAR_BTN_BG: 'var(--item-active-background-color)', + MAIN_DISABLED_BG: 'var(--disabled-background-color)', + theme: 'var(--primary-color)', + DESC_TEXT: 'var(--TITLE_TEXT)', + HOVER_BG: 'var(--item-hover-background-color)', + guide_cluster: '#ee6760', + guide_upstream: '#f9a429', + guide_api: '#71d24d', + guide_publishApi: '#5884ff', + guide_final: '#915bf9', + table_text: 'var(--table-text-color)', + status_success:'#138913', + status_fail:"#ff3b30", + status_update:"#03a9f4", + status_pending:"#ffa500", + status_offline:"#8f8e93", + A_HOVER:'var(--button-primary-hover-background-color)' + }, + spacing: { + mbase: 'var(--FORM_SPAN)', + label: '12px', // 选择器和label之间的间距,待删 + btnbase: 'var(--LAYOUT_MARGIN)', // x方向的间距 + btnybase: 'var(--LAYOUT_MARGIN)', // y轴方向的间距 + btnrbase: '20px', // 页面最右侧边距20px + formtop: 'var(--FORM_SPAN)', + icon: '5px', + blockbase: '40px', + DEFAULT_BORDER_RADIUS: 'var(--border-radius)', + TREE_TITLE:'var(--small-padding) var(--LAYOUT_PADDING);' + }, + borderColor: { + 'color-base': 'var(--border-color)' + } + } + }, + plugins: [], + corePlugins: { + preflight: false, + }, + } + + \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 00000000..4025916c --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./packages/core/tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "jsx": "react", + "types": ["jest", "node"], + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "noImplicitAny": false + }, + "include": ["**/*.test.ts", "**/*.test.tsx", "jest.setup.js"], + "exclude": ["node_modules"] +} \ No newline at end of file diff --git a/gateway/admin/admin.go b/gateway/admin/admin.go new file mode 100644 index 00000000..b1a95b23 --- /dev/null +++ b/gateway/admin/admin.go @@ -0,0 +1,25 @@ +package admin + +import "context" + +type adminClient struct { + address []string +} + +func (a *adminClient) Info(ctx context.Context) (*Info, error) { + return callHttp[Info](ctx, a.address, "GET", "/system/info", nil) +} + +func Admin(address ...string) IApintoAdmin { + return &adminClient{address: formatAddr(address)} +} +func (a *adminClient) Version(ctx context.Context) (*Version, error) { + return callHttp[Version](ctx, a.address, "GET", "/system/version", nil) +} +func (a *adminClient) Ping(ctx context.Context) error { + _, err := a.Version(ctx) + if err != nil { + return err + } + return err +} diff --git a/gateway/admin/client.go b/gateway/admin/client.go new file mode 100644 index 00000000..ce396565 --- /dev/null +++ b/gateway/admin/client.go @@ -0,0 +1,9 @@ +package admin + +import "context" + +type IApintoAdmin interface { + Ping(ctx context.Context) error + Info(ctx context.Context) (*Info, error) + Version(ctx context.Context) (*Version, error) +} diff --git a/gateway/admin/http.go b/gateway/admin/http.go new file mode 100644 index 00000000..08296ac0 --- /dev/null +++ b/gateway/admin/http.go @@ -0,0 +1,66 @@ +package admin + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" +) + +var ( + httpClient = &http.Client{} + ErrorInvalidAdminAddress = errors.New("invalid address") +) + +func callHttp[M any](ctx context.Context, address []string, method string, path string, body []byte) (*M, error) { + if len(address) == 0 { + return nil, ErrorInvalidAdminAddress + } + method = strings.ToUpper(method) + var response *http.Response + var err error + for _, addr := range address { + url := fmt.Sprint(addr, strings.TrimPrefix(path, "/")) + response, err = doRequest(ctx, method, url, body) + if err != nil { + continue + } + if response.StatusCode != 200 { + continue + } + break + } + if err != nil { + return nil, err + } + + if response.StatusCode != 200 { + return nil, fmt.Errorf("http status code is %d", response.StatusCode) + } + data, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + m := new(M) + err = json.Unmarshal(data, m) + if err != nil { + return nil, err + } + return m, nil + +} +func doRequest(ctx context.Context, method string, url string, body []byte) (*http.Response, error) { + + request, err := http.NewRequestWithContext(ctx, method, url, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + request.Header.Set("User-Agent", "aoplatform") + request.Header.Set("content-type", "application/json") + return httpClient.Do(request) +} diff --git a/gateway/admin/model.go b/gateway/admin/model.go new file mode 100644 index 00000000..ec5b6848 --- /dev/null +++ b/gateway/admin/model.go @@ -0,0 +1,16 @@ +package admin + +type Node struct { + Id string `json:"id,omitempty" yaml:"id"` + Name string `json:"name,omitempty" yaml:"name"` + Peer []string `json:"peer,omitempty" yaml:"peer"` + Admin []string `json:"admin,omitempty" yaml:"admin"` + Server []string `json:"server,omitempty" yaml:"server"` + Leader bool `json:"leader,omitempty" yaml:"leader"` +} +type Info struct { + Cluster string `yaml:"cluster" json:"cluster,omitempty"` + Nodes []*Node `yaml:"nodes" json:"nodes,omitempty"` +} +type Version struct { +} diff --git a/gateway/admin/util.go b/gateway/admin/util.go new file mode 100644 index 00000000..a7b0c67c --- /dev/null +++ b/gateway/admin/util.go @@ -0,0 +1,17 @@ +package admin + +import "strings" + +func formatAddr(address []string) []string { + ra := make([]string, 0, len(address)) + for _, a := range address { + if a != "" { + if !strings.HasPrefix(a, "http://") && !strings.HasPrefix(a, "https://") { + a = "http://" + a + } + + ra = append(ra, strings.TrimSuffix(a, "/")+"/") + } + } + return ra +} diff --git a/gateway/apinto/application.go b/gateway/apinto/application.go new file mode 100644 index 00000000..31472eae --- /dev/null +++ b/gateway/apinto/application.go @@ -0,0 +1,92 @@ +package apinto + +import ( + "context" + "strings" + + "github.com/APIParkLab/APIPark/gateway/apinto/auth" + + "github.com/APIParkLab/APIPark/gateway/apinto/entity" + + "github.com/APIParkLab/APIPark/gateway" + admin_client "github.com/eolinker/eosc/process-admin/client" +) + +var _ gateway.IApplicationClient = &ApplicationClient{} + +func NewApplicationClient(client admin_client.Client) *ApplicationClient { + return &ApplicationClient{client: client} +} + +type ApplicationClient struct { + client admin_client.Client +} + +func (a *ApplicationClient) Online(ctx context.Context, applications ...*gateway.ApplicationRelease) error { + err := a.client.Begin(ctx) + if err != nil { + return err + } + for _, app := range applications { + err = a.client.Set(ctx, genWorkerID(app.ID, ProfessionApplication), a.toApinto(app)) + if err != nil { + a.client.Rollback(ctx) + return err + } + } + return a.client.Commit(ctx) +} + +func (a *ApplicationClient) Offline(ctx context.Context, applications ...*gateway.ApplicationRelease) error { + err := a.client.Begin(ctx) + if err != nil { + return err + } + for _, app := range applications { + err = a.client.Del(ctx, genWorkerID(app.ID, ProfessionApplication)) + if err != nil { + a.client.Rollback(ctx) + return err + } + } + return a.client.Commit(ctx) +} + +func (a *ApplicationClient) toApinto(app *gateway.ApplicationRelease) interface{} { + auths := make([]*entity.Authorization, 0, len(app.Authorizations)) + for _, info := range app.Authorizations { + driver, has := auth.Get(info.Type) + if !has { + continue + } + auths = append(auths, &entity.Authorization{ + Type: info.Type, + Position: strings.ToLower(info.Position), + TokenName: info.TokenName, + Config: driver.ToConfig(info.Config), + Users: []*entity.AuthUser{ + { + Expire: info.Expire, + Pattern: driver.ToPattern(info.Config), + HideCredential: info.HideCredential, + }, + }, + }) + } + labels := make(map[string]string) + if app.Labels != nil { + labels = app.Labels + } + return &entity.Application{ + BasicInfo: &entity.BasicInfo{ + ID: genWorkerID(app.ID, ProfessionApplication), + Name: app.ID, + Description: app.Description, + Driver: "app", + Version: app.Version, + Matches: nil, + }, + Labels: labels, + Authorizations: auths, + } +} diff --git a/gateway/apinto/auth/apikey.go b/gateway/apinto/auth/apikey.go new file mode 100644 index 00000000..ee72017f --- /dev/null +++ b/gateway/apinto/auth/apikey.go @@ -0,0 +1,27 @@ +package auth + +func init() { + a := NewApikey() + Register(a.Name(), a) +} + +func NewApikey() *Apikey { + return &Apikey{} +} + +type Apikey struct { +} + +func (a *Apikey) Name() string { + return "apikey" +} + +func (a *Apikey) ToPattern(cfg map[string]interface{}) interface{} { + result := make(map[string]interface{}) + result["apikey"] = cfg["apikey"] + return result +} + +func (a *Apikey) ToConfig(cfg map[string]interface{}) interface{} { + return nil +} diff --git a/gateway/apinto/auth/basic.go b/gateway/apinto/auth/basic.go new file mode 100644 index 00000000..8bab9c5f --- /dev/null +++ b/gateway/apinto/auth/basic.go @@ -0,0 +1,28 @@ +package auth + +func init() { + b := NewBasic() + Register(b.Name(), b) +} + +func NewBasic() *Basic { + return &Basic{} +} + +type Basic struct { +} + +func (b *Basic) Name() string { + return "basic" +} + +func (b *Basic) ToPattern(cfg map[string]interface{}) interface{} { + result := make(map[string]interface{}) + result["username"] = cfg["user_name"] + result["password"] = cfg["password"] + return result +} + +func (b *Basic) ToConfig(cfg map[string]interface{}) interface{} { + return nil +} diff --git a/gateway/apinto/auth/manager.go b/gateway/apinto/auth/manager.go new file mode 100644 index 00000000..86bcdb4c --- /dev/null +++ b/gateway/apinto/auth/manager.go @@ -0,0 +1,39 @@ +package auth + +import "github.com/eolinker/eosc" + +type IAuthDriver interface { + Name() string + ToPattern(cfg map[string]interface{}) interface{} + ToConfig(cfg map[string]interface{}) interface{} +} + +type DriverManager struct { + drivers eosc.Untyped[string, IAuthDriver] +} + +func NewAuthDriverManager() *DriverManager { + return &DriverManager{ + drivers: eosc.BuildUntyped[string, IAuthDriver](), + } +} + +var ( + authDriverManager = NewAuthDriverManager() +) + +func (a *DriverManager) Register(name string, driver IAuthDriver) { + a.drivers.Set(name, driver) +} + +func (a *DriverManager) Get(name string) (IAuthDriver, bool) { + return a.drivers.Get(name) +} + +func Register(name string, driver IAuthDriver) { + authDriverManager.Register(name, driver) +} + +func Get(name string) (IAuthDriver, bool) { + return authDriverManager.Get(name) +} diff --git a/gateway/apinto/client.go b/gateway/apinto/client.go new file mode 100644 index 00000000..9c292ecb --- /dev/null +++ b/gateway/apinto/client.go @@ -0,0 +1,83 @@ +package apinto + +import ( + "context" + "strings" + + "github.com/APIParkLab/APIPark/gateway" + admin_client "github.com/eolinker/eosc/process-admin/client" +) + +var _ gateway.IClientDriver = (*ClientDriver)(nil) + +type ClientDriver struct { + client admin_client.Client +} + +func (c *ClientDriver) Close(ctx context.Context) error { + if c.client != nil { + return c.client.Close() + } + return nil +} + +func (c *ClientDriver) Commit(ctx context.Context) error { + return c.client.Commit(ctx) +} + +func (c *ClientDriver) Rollback(ctx context.Context) error { + return c.Rollback(ctx) +} + +func (c *ClientDriver) Begin(ctx context.Context) error { + return c.client.Begin(ctx) +} + +func (c *ClientDriver) Project() gateway.IProjectClient { + return NewProjectClient(c.client) +} + +func (c *ClientDriver) Service() gateway.IServiceClient { + return NewServiceClient(c.client) +} + +func (c *ClientDriver) Subscribe() gateway.ISubscribeClient { + return NewSubscribeClient(c.client) +} + +func (c *ClientDriver) Application() gateway.IApplicationClient { + return NewApplicationClient(c.client) +} + +func (c *ClientDriver) Dynamic(resource string) (gateway.IDynamicClient, error) { + return NewDynamicClient(c.client, resource) +} + +func (c *ClientDriver) PluginSetting() gateway.IPluginSetting { + return NewPluginSettingClient(c.client) +} + +func NewClientDriver(cfg *gateway.ClientConfig) (*ClientDriver, error) { + address := make([]string, 0, len(cfg.Addresses)) + for _, addr := range cfg.Addresses { + addr = strings.TrimPrefix(addr, "http://") + addr = strings.TrimPrefix(addr, "https://") + address = append(address, addr) + } + client, err := admin_client.New(address...) + if err != nil { + return nil, err + } + return &ClientDriver{ + client: client, + }, nil +} + +func genWorkerID(id string, profession string) string { + + suffix := "@" + profession + if strings.HasSuffix(id, suffix) { + return id + } + return id + suffix +} diff --git a/gateway/apinto/driver/api.go b/gateway/apinto/driver/api.go new file mode 100644 index 00000000..6480e2b9 --- /dev/null +++ b/gateway/apinto/driver/api.go @@ -0,0 +1,41 @@ +package driver + +import ( + "context" + "github.com/APIParkLab/APIPark/gateway/apinto/entity" + admin_client "github.com/eolinker/eosc/process-admin/client" +) + +var ( + apiPublishHandlers []ApiPublishHandler + apiDeleteHandlers []ApiDeleteHandler +) + +func RegisterApiPublishHandler(handler ApiPublishHandler) { + apiPublishHandlers = append(apiPublishHandlers, handler) +} +func RegisterApiDeleteHandler(handler ApiDeleteHandler) { + apiDeleteHandlers = append(apiDeleteHandlers, handler) +} + +type ApiPublishHandler func(ctx context.Context, client admin_client.Client, api *entity.Router, extends map[string]any) error + +type ApiDeleteHandler func(ctx context.Context, client admin_client.Client, api *entity.Router) error + +func ApiPublish(ctx context.Context, client admin_client.Client, api *entity.Router, extends map[string]any) error { + for _, handler := range apiPublishHandlers { + if err := handler(ctx, client, api, extends); err != nil { + return err + } + } + return nil +} + +func ApiDelete(ctx context.Context, client admin_client.Client, api *entity.Router) error { + for _, handler := range apiDeleteHandlers { + if err := handler(ctx, client, api); err != nil { + return err + } + } + return nil +} diff --git a/gateway/apinto/dynamic.go b/gateway/apinto/dynamic.go new file mode 100644 index 00000000..3654411b --- /dev/null +++ b/gateway/apinto/dynamic.go @@ -0,0 +1,108 @@ +package apinto + +import ( + "context" + "errors" + + "github.com/eolinker/go-common/encoding" + + "github.com/eolinker/eosc/process-admin/cmd/proto" + + "github.com/APIParkLab/APIPark/gateway/apinto/entity" + + "github.com/APIParkLab/APIPark/gateway" + admin_client "github.com/eolinker/eosc/process-admin/client" +) + +var _ gateway.IDynamicClient = &DynamicClient{} + +func NewDynamicClient(client admin_client.Client, resource string) (*DynamicClient, error) { + cfg, has := dynamicResourceMap[resource] + if !has { + return nil, errors.New("resource not found") + + } + + return &DynamicClient{client: client, profession: cfg.Profession, driver: cfg.Driver}, nil +} + +type DynamicClient struct { + profession string + driver string + client admin_client.Client +} + +func (d *DynamicClient) Version(ctx context.Context, resourceId string) (string, error) { + worker, err := d.client.Get(ctx, genWorkerID(resourceId, d.profession)) + if err != nil { + return "", err + } + if len(worker) == 0 { + return "", nil + } + var item entity.BasicInfo + err = worker.Scan(encoding.Json[entity.BasicInfo](&item)) + if err != nil { + return "", err + } + return item.Version, nil +} + +func (d *DynamicClient) Versions(ctx context.Context, matchLabels map[string]string) (map[string]string, error) { + workers, err := d.client.MatchLabels(ctx, d.profession, matchLabels) + if err != nil { + if errors.Is(err, proto.Nil) { + return nil, nil + } + return nil, err + } + versions := make(map[string]string) + for _, worker := range workers { + var item entity.BasicInfo + err = worker.Scan(encoding.Json[entity.BasicInfo](&item)) + if err != nil { + return nil, err + } + versions[item.Name] = item.Version + } + return versions, nil +} + +func (d *DynamicClient) Online(ctx context.Context, resources ...*gateway.DynamicRelease) error { + err := d.client.Begin(ctx) + if err != nil { + return err + } + for _, r := range resources { + id := genWorkerID(r.ID, d.profession) + worker := entity.NewWorkerItem[map[string]interface{}](&entity.BasicInfo{ + ID: id, + Name: r.ID, + Description: r.Description, + Driver: d.driver, + Version: r.Version, + Matches: r.MatchLabels, + }, &r.Attr) + err = d.client.Set(ctx, id, worker) + if err != nil { + d.client.Rollback(ctx) + return err + } + } + return d.client.Commit(ctx) +} + +func (d *DynamicClient) Offline(ctx context.Context, resources ...*gateway.DynamicRelease) error { + err := d.client.Begin(ctx) + if err != nil { + return err + } + for _, r := range resources { + err = d.client.Del(ctx, genWorkerID(r.ID, d.profession)) + if err != nil { + d.client.Rollback(ctx) + return err + } + } + return d.client.Commit(ctx) +} diff --git a/gateway/apinto/entity/application.go b/gateway/apinto/entity/application.go new file mode 100644 index 00000000..bcc9f9a5 --- /dev/null +++ b/gateway/apinto/entity/application.go @@ -0,0 +1,21 @@ +package entity + +type Application struct { + *BasicInfo + Labels map[string]string `json:"labels"` + Authorizations []*Authorization `json:"auth"` +} + +type Authorization struct { + Type string `json:"type"` + Position string `json:"position"` + TokenName string `json:"token_name"` + Config interface{} `json:"config"` + Users []*AuthUser `json:"users"` +} + +type AuthUser struct { + Expire int64 `json:"expire"` + Pattern interface{} `json:"pattern"` + HideCredential bool `json:"hide_credential"` +} diff --git a/gateway/apinto/entity/router.go b/gateway/apinto/entity/router.go new file mode 100644 index 00000000..5b82feb9 --- /dev/null +++ b/gateway/apinto/entity/router.go @@ -0,0 +1,203 @@ +package entity + +import ( + "fmt" + "net/textproto" + "strings" + + "github.com/APIParkLab/APIPark/common" + + "github.com/APIParkLab/APIPark/common/enum" + "github.com/APIParkLab/APIPark/gateway" +) + +const apintoRestfulRegexp = "([0-9a-zA-Z-_]+)" + +type Router struct { + *BasicInfo + Listen int `json:"listen"` + Host []string `json:"host"` + Method []string `json:"method"` + Protocols []string `json:"protocols"` + Location string `json:"location"` + Rules []*Rule `json:"rules"` + Service string `json:"service"` + Plugins map[string]*Plugin `json:"plugins"` + Retry int `json:"retry"` + TimeOut int `json:"time_out"` + Labels map[string]string `json:"labels"` +} + +type Rule struct { + Type string `json:"type"` + Name string `json:"name"` + Value string `json:"value"` +} + +type Plugin struct { + Disable bool `json:"disable"` + Config interface{} `json:"config"` +} + +type PluginProxyRewriteV2Config struct { + PathType string `json:"path_type"` + StaticPath string `json:"static_path,omitempty"` //path_type=static启用 + PrefixPath []*PrefixPath `json:"prefix_path,omitempty"` //path_type=prefix 启用 + RegexPath []*RegexPath `json:"regex_path,omitempty"` //path_type=regex 启用 + NotMatchErr bool `json:"not_match_err"` + HostRewrite bool `json:"host_rewrite"` + Host string `json:"host,omitempty"` + Headers map[string]string `json:"headers"` +} + +type RegexPath struct { + RegexPathMatch string `json:"regex_path_match"` + RegexPathReplace string `json:"regex_path_replace"` +} + +type PrefixPath struct { + PrefixPathMatch string `json:"prefix_path_match"` + PrefixPathReplace string `json:"prefix_path_replace"` +} + +func ToRouter(r *gateway.ApiRelease, version string, matches map[string]string) *Router { + headers := make(map[string]string) + for _, h := range r.ProxyHeaders { + key := textproto.CanonicalMIMEHeaderKey(h.Key) + switch h.Opt { + case enum.HeaderOptTypeAdd: + headers[key] = h.Value + case enum.HeaderOptTypeDelete: + headers[key] = "" + } + } + rewritePlugin := PluginProxyRewriteV2Config{ + NotMatchErr: true, + HostRewrite: false, + Headers: headers, + } + //若请求路径包含restful参数 + if common.IsRestfulPath(r.Path) { + rewritePlugin.PathType = "regex" //正则替换 + + //如果转发路径包含restful参数 + if common.IsRestfulPath(r.ProxyPath) { + r.ProxyPath = formatProxyPath(r.Path, r.ProxyPath) + } + rewritePlugin.RegexPath = []*RegexPath{ + { + RegexPathMatch: fmt.Sprintf("^%s$", common.ReplaceRestfulPath(r.Path, apintoRestfulRegexp)), + RegexPathReplace: r.ProxyPath, + }, + } + r.Path = fmt.Sprintf("~=^%s$", common.ReplaceRestfulPath(r.Path, apintoRestfulRegexp)) + } else { + rewritePlugin.PathType = "prefix" //前缀替换 + rewritePlugin.PrefixPath = []*PrefixPath{ + { + PrefixPathMatch: strings.TrimSuffix(r.Path, "*"), + PrefixPathReplace: r.ProxyPath, + }, + } + } + + rules := make([]*Rule, 0, len(r.Rules)) + for _, m := range r.Rules { + rule := &Rule{ + Type: m.Position, + Name: m.Key, + Value: "", + } + + if m.Position == enum.MatchPositionHeader { + rule.Name = textproto.CanonicalMIMEHeaderKey(rule.Name) + } + + switch m.MatchType { + case enum.MatchTypeEqual: + rule.Value = m.Pattern + case enum.MatchTypePrefix: + rule.Value = fmt.Sprintf("%s*", m.Pattern) + case enum.MatchTypeSuffix: + rule.Value = fmt.Sprintf("*%s", m.Pattern) + case enum.MatchTypeSubstr: + rule.Value = fmt.Sprintf("*%s*", m.Pattern) + case enum.MatchTypeUnEqual: + rule.Value = fmt.Sprintf("!=%s", m.Pattern) + case enum.MatchTypeNull: + rule.Value = "$" + case enum.MatchTypeExist: + rule.Value = "**" + case enum.MatchTypeUnExist: + rule.Value = "!" + case enum.MatchTypeRegexp: + rule.Value = fmt.Sprintf("~=%s", m.Pattern) + case enum.MatchTypeRegexpG: + rule.Value = fmt.Sprintf("~*=%s", m.Pattern) + case enum.MatchTypeAny: + rule.Value = "*" + } + + rules = append(rules, rule) + } + plugin := map[string]*Plugin{ + "proxy_rewrite": { + Disable: false, + Config: rewritePlugin, + }, + } + for k, v := range r.Plugins { + plugin[k] = &Plugin{ + Disable: false, + Config: v, + } + } + hosts := make([]string, 0, len(r.Host)) + if len(r.Host) > 0 { + hosts = r.Host + } + labels := make(map[string]string) + if r.Labels != nil { + labels = r.Labels + } + + return &Router{ + BasicInfo: &BasicInfo{ + ID: fmt.Sprintf("%s@router", r.ID), + Name: r.ID, + Description: r.Description, + Driver: "http", + Version: version, + Matches: matches, + }, + Host: hosts, + Method: r.Method, + Location: r.Path, + Rules: rules, + Service: r.Service, + Plugins: plugin, + Retry: r.Retry, + TimeOut: r.Timeout, + Labels: labels, + Protocols: []string{"http", "https"}, + } +} + +// formatProxyPath 格式化转发路径上,用于转发重写插件正则替换 比如 请求路径 /path/{A}/{B} 原转发路径:/path/{B} 格式化后 新转发路径: /path/$2 +func formatProxyPath(requestPath, proxyPath string) string { + restfulSet := make(map[string]string) + newProxyPath := proxyPath + rList := strings.Split(requestPath, "/") + i := 1 + for _, param := range rList { + if common.IsRestfulParam(param) { + restfulSet[param] = fmt.Sprintf("$%d", i) + i += 1 + } + } + + for param, order := range restfulSet { + newProxyPath = strings.ReplaceAll(newProxyPath, param, order) + } + return newProxyPath +} diff --git a/gateway/apinto/entity/service.go b/gateway/apinto/entity/service.go new file mode 100644 index 00000000..4e371bb8 --- /dev/null +++ b/gateway/apinto/entity/service.go @@ -0,0 +1,37 @@ +package entity + +import ( + "fmt" + "strings" + + "github.com/APIParkLab/APIPark/gateway" +) + +type Service struct { + *BasicInfo + Nodes []string `json:"nodes"` + PassHost string `json:"pass_host"` + Scheme string `json:"scheme"` + Timeout int `json:"timeout"` + Balance string `json:"balance"` + Labels map[string]string `json:"labels"` +} + +func ToService(s *gateway.UpstreamRelease, version string, matches map[string]string) *Service { + return &Service{ + BasicInfo: &BasicInfo{ + ID: fmt.Sprintf("%s@service", s.ID), + Name: s.ID, + Description: s.Description, + Driver: "http", + Version: version, + Matches: matches, + }, + Nodes: s.Nodes, + PassHost: s.PassHost, + Scheme: strings.ToUpper(s.Scheme), + Timeout: s.Timeout, + Balance: s.Balance, + Labels: s.Labels, + } +} diff --git a/gateway/apinto/entity/worker.go b/gateway/apinto/entity/worker.go new file mode 100644 index 00000000..5b28b228 --- /dev/null +++ b/gateway/apinto/entity/worker.go @@ -0,0 +1,60 @@ +package entity + +import ( + "encoding/json" +) + +type BasicInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Driver string `json:"driver"` + Version string `json:"version"` + Matches map[string]string `json:"matches"` +} + +type WorkerItem[T any] struct { + Basic *BasicInfo + Attr *T +} + +func NewWorkerItem[T any](basic *BasicInfo, attr *T) *WorkerItem[T] { + return &WorkerItem[T]{Basic: basic, Attr: attr} +} + +func (w *WorkerItem[T]) MarshalJSON() ([]byte, error) { + data, err := json.Marshal(w.Attr) + if err != nil { + return nil, err + } + + attr := make(map[string]interface{}) + err = json.Unmarshal(data, &attr) + if err != nil { + return nil, err + } + attr["id"] = w.Basic.ID + attr["name"] = w.Basic.Name + attr["description"] = w.Basic.Description + attr["driver"] = w.Basic.Driver + attr["version"] = w.Basic.Version + attr["matches"] = w.Basic.Matches + + return json.Marshal(attr) +} + +func (w *WorkerItem[T]) UnmarshalJSON(bytes []byte) error { + attr := new(T) + err := json.Unmarshal(bytes, attr) + if err != nil { + return err + } + basicInfo := new(BasicInfo) + err = json.Unmarshal(bytes, basicInfo) + if err != nil { + return err + } + w.Basic = basicInfo + w.Attr = attr + return nil +} diff --git a/gateway/apinto/extends/cors.go b/gateway/apinto/extends/cors.go new file mode 100644 index 00000000..f38fcb6c --- /dev/null +++ b/gateway/apinto/extends/cors.go @@ -0,0 +1,21 @@ +/* +处理cors扩展, 对启用了cors的api,添加 cors信息 +从apinto移除api时, 移除对应的cors信息 +*/ +package extends + +import ( + "context" + "github.com/APIParkLab/APIPark/gateway/apinto/driver" + "github.com/APIParkLab/APIPark/gateway/apinto/entity" + admin_client "github.com/eolinker/eosc/process-admin/client" +) + +func init() { + driver.RegisterApiPublishHandler(func(ctx context.Context, client admin_client.Client, api *entity.Router, extends map[string]any) error { + return nil + }) + driver.RegisterApiDeleteHandler(func(ctx context.Context, client admin_client.Client, api *entity.Router) error { + return nil + }) +} diff --git a/gateway/apinto/factory.go b/gateway/apinto/factory.go new file mode 100644 index 00000000..440a82cb --- /dev/null +++ b/gateway/apinto/factory.go @@ -0,0 +1,18 @@ +package apinto + +import ( + "github.com/APIParkLab/APIPark/gateway" +) + +func init() { + gateway.Register("apinto", &Factory{}) +} + +var _ gateway.IFactory = &Factory{} + +type Factory struct { +} + +func (f *Factory) Create(config *gateway.ClientConfig) (gateway.IClientDriver, error) { + return NewClientDriver(config) +} diff --git a/gateway/apinto/plugin.go b/gateway/apinto/plugin.go new file mode 100644 index 00000000..752aa5e3 --- /dev/null +++ b/gateway/apinto/plugin.go @@ -0,0 +1,45 @@ +package apinto + +import ( + "context" + + "github.com/APIParkLab/APIPark/gateway/apinto/plugin" + + "github.com/APIParkLab/APIPark/gateway" + admin_client "github.com/eolinker/eosc/process-admin/client" +) + +var _ gateway.IPluginSetting = &PluginSettingClient{} + +type PluginSettingClient struct { + client admin_client.Client +} + +func (p *PluginSettingClient) Init(ctx context.Context) error { + //data, err := json.Marshal(map[string]interface{}{ + // "plugins": plugin.GetGlobalPluginConf(), + //}) + //log.Println(string(data), err) + return p.client.SSet(ctx, "plugin", map[string]interface{}{ + "plugins": plugin.GetGlobalPluginConf(), + }) +} + +func (p *PluginSettingClient) Set(ctx context.Context, configs []*gateway.PluginConfig) error { + globalPlugins := make([]*plugin.GlobalPlugin, 0, len(configs)) + for _, cfg := range configs { + globalPlugins = append(globalPlugins, &plugin.GlobalPlugin{ + Config: cfg.Config, + Id: cfg.Id, + Name: cfg.Name, + Status: cfg.Status, + }) + } + return p.client.SSet(ctx, "plugin", map[string]interface{}{ + "plugins": globalPlugins, + }) +} + +func NewPluginSettingClient(client admin_client.Client) *PluginSettingClient { + return &PluginSettingClient{client: client} +} diff --git a/gateway/apinto/plugin/apinto_plugin.yml b/gateway/apinto/plugin/apinto_plugin.yml new file mode 100644 index 00000000..f82c811d --- /dev/null +++ b/gateway/apinto/plugin/apinto_plugin.yml @@ -0,0 +1,66 @@ +- + id: eolinker.com:apinto:access_log + name: access_log + status: global +- + id: eolinker.com:apinto:monitor + name: monitor + status: global +- + id: eolinker.com:apinto:proxy_rewrite_v2 + name: proxy_rewrite + status: enable +- + id: eolinker.com:apinto:extra_params + name: extra_params + status: enable +- + id: eolinker.com:apinto:plugin_app + name: app + status: global +- + id: eolinker.com:apinto:access_relational + name: access_relational + status: global + config: + rules: + - a: "service_of_api:#{api}" + b: "subscription_service:#{application}" + response: + status_code: 403 + content_typ: "text/plan" + charset: "utf-8" + body: "Forbidden" +- + id: eolinker.com:apinto:strategy-plugin-visit + name: strategy_visit + status: global + rely: eolinker.com:apinto:plugin_app +- + id: eolinker.com:apinto:strategy-plugin-grey + name: strategy_grey + status: global + rely: eolinker.com:apinto:plugin_app +- + id: eolinker.com:apinto:strategy-plugin-limiting + name: strategy_limiting + status: global + rely: eolinker.com:apinto:plugin_app + config: + cache: redis@output +- + id: eolinker.com:apinto:strategy-plugin-fuse + name: strategy_fuse + status: global + rely: eolinker.com:apinto:plugin_app + config: + cache: redis@output +- + id: eolinker.com:apinto:strategy-plugin-cache + name: strategy_cache + status: global + rely: eolinker.com:apinto:plugin_app + config: + cache: redis@output + + diff --git a/gateway/apinto/plugin/global.go b/gateway/apinto/plugin/global.go new file mode 100644 index 00000000..66d07246 --- /dev/null +++ b/gateway/apinto/plugin/global.go @@ -0,0 +1,37 @@ +package plugin + +import ( + _ "embed" + + "gopkg.in/yaml.v3" +) + +//go:embed apinto_plugin.yml +var pluginData []byte + +type GlobalPlugin struct { + Config interface{} `json:"config,omitempty"` //Plugin***Config + Id string `json:"id"` + InitConfig interface{} `json:"init_config,omitempty"` + Name string `json:"name"` //名称 + Status string `json:"status"` //enable,disable,global + Rely string `json:"rely"` //依赖哪个插件 +} + +var pluginConf []*GlobalPlugin + +func init() { + var err error + + pc := make([]*GlobalPlugin, 0) + err = yaml.Unmarshal(pluginData, &pc) + if err != nil { + panic(err) + } + + pluginConf = pc +} + +func GetGlobalPluginConf() []*GlobalPlugin { + return pluginConf +} diff --git a/gateway/apinto/profession.go b/gateway/apinto/profession.go new file mode 100644 index 00000000..b75a9918 --- /dev/null +++ b/gateway/apinto/profession.go @@ -0,0 +1,50 @@ +package apinto + +const ( + ProfessionOutput = "output" + ProfessionCertificate = "certificate" + ProfessionRouter = "router" + ProfessionApplication = "app" + ProfessionService = "service" +) + +var dynamicResourceMap = map[string]Worker{ + "file-access-log": { + Profession: ProfessionOutput, + Driver: "file", + }, + "http-access-log": { + Profession: ProfessionOutput, + Driver: "http_output", + }, + "nsqd-access-log": { + Profession: ProfessionOutput, + Driver: "nsqd", + }, + "syslog-access-log": { + Profession: ProfessionOutput, + Driver: "syslog_output", + }, + "kafka-access-log": { + Profession: ProfessionOutput, + Driver: "kafka_output", + }, + "influxdbv2": { + Profession: ProfessionOutput, + Driver: "influxdbv2", + }, + "redis": { + Profession: ProfessionOutput, + Driver: "redis", + }, + // 证书 + "certificate": { + Profession: ProfessionCertificate, + Driver: "server", + }, +} + +type Worker struct { + Profession string + Driver string +} diff --git a/gateway/apinto/project.go b/gateway/apinto/project.go new file mode 100644 index 00000000..9d76215b --- /dev/null +++ b/gateway/apinto/project.go @@ -0,0 +1,188 @@ +package apinto + +import ( + "context" + "errors" + "fmt" + "github.com/APIParkLab/APIPark/gateway/apinto/driver" + + "github.com/eolinker/eosc/process-admin/cmd/proto" + + "github.com/eolinker/go-common/encoding" + + "github.com/eolinker/go-common/utils" + + "github.com/APIParkLab/APIPark/gateway/apinto/entity" + + "github.com/APIParkLab/APIPark/gateway" + admin_client "github.com/eolinker/eosc/process-admin/client" +) + +var _ gateway.IProjectClient = &ProjectClient{} + +func init() { + driver.RegisterApiPublishHandler(func(ctx context.Context, client admin_client.Client, api *entity.Router, extends map[string]any) error { + return client.Set(ctx, api.ID, api) + + }) +} + +func NewProjectClient(client admin_client.Client) *ProjectClient { + return &ProjectClient{client: client} +} + +type ProjectClient struct { + client admin_client.Client +} + +func (p *ProjectClient) Online(ctx context.Context, projects ...*gateway.ProjectRelease) error { + err := p.client.Begin(ctx) + if err != nil { + return err + } + for _, project := range projects { + err = p.online(ctx, project) + if err != nil { + p.client.Rollback(ctx) + return err + } + } + p.client.Commit(ctx) + return nil +} + +func (p *ProjectClient) online(ctx context.Context, project *gateway.ProjectRelease) error { + if project == nil { + return nil + } + if project.Upstream == nil { + return fmt.Errorf("upstream is nil") + } + matches := map[string]string{ + "project": project.Id, + } + + upstreams, err := matchLabels[entity.Service](ctx, p.client, ProfessionService, matches) + if err != nil { + if !errors.Is(err, proto.Nil) { + return err + } + } + upstreamMap := utils.SliceToMap(upstreams, func(t *entity.Service) string { + return t.ID + }) + + upstreamId := genWorkerID(project.Upstream.ID, ProfessionService) + err = p.client.Set(ctx, upstreamId, entity.ToService(project.Upstream, project.Version, matches)) + if err != nil { + return err + } + delete(upstreamMap, upstreamId) + routers, err := matchLabels[entity.Router](ctx, p.client, ProfessionRouter, matches) + if err != nil { + if !errors.Is(err, proto.Nil) { + return err + } + } + routerMap := utils.SliceToMap(routers, func(t *entity.Router) string { + return t.ID + }) + + for _, api := range project.Apis { + id := genWorkerID(api.ID, ProfessionRouter) + if api.Labels == nil { + api.Labels = make(map[string]string) + } + api.Service = upstreamId + api.Labels["provider"] = project.Id + routerInfo := entity.ToRouter(api, project.Version, matches) + + err = driver.ApiPublish(ctx, p.client, routerInfo, api.Extends) + if err != nil { + return err + } + delete(routerMap, id) + } + // 删除多余配置 + for _, v := range routerMap { + err := driver.ApiDelete(ctx, p.client, v) + if err != nil { + return err + } + err = p.client.Del(ctx, v.ID) + if err != nil { + return err + } + + } + for id := range upstreamMap { + err = p.client.Del(ctx, id) + if err != nil { + return err + } + } + + return nil +} + +func (p *ProjectClient) Offline(ctx context.Context, projects ...*gateway.ProjectRelease) error { + err := p.client.Begin(ctx) + if err != nil { + return err + } + for _, project := range projects { + err = p.delete(ctx, project.Id) + if err != nil { + p.client.Rollback(ctx) + return err + } + } + + return p.client.Commit(ctx) +} + +func (p *ProjectClient) delete(ctx context.Context, id string) error { + err := p.deleteByLabels(ctx, ProfessionRouter, map[string]string{"project": id}) + if err != nil { + return err + } + return p.deleteByLabels(ctx, ProfessionService, map[string]string{"project": id}) +} +func matchLabels[T any](ctx context.Context, client admin_client.Client, profession string, labels map[string]string, t ...[]*T) ([]*T, error) { + list, err := client.MatchLabels(ctx, profession, labels) + if err != nil { + return nil, err + } + var items = make([]*T, 0, len(list)) + for _, item := range list { + var basicItem = new(T) + err = item.Scan(encoding.Json(basicItem)) + if err != nil { + return nil, err + } + items = append(items, basicItem) + } + return items, nil +} +func (p *ProjectClient) matchLabels(ctx context.Context, profession string, labels map[string]string) ([]*entity.BasicInfo, error) { + return matchLabels[entity.BasicInfo](ctx, p.client, profession, labels) +} + +func (p *ProjectClient) deleteByLabels(ctx context.Context, profession string, labels map[string]string) error { + list, err := p.client.MatchLabels(ctx, profession, labels) + if err != nil { + return err + } + for _, item := range list { + var basicItem entity.BasicInfo + err = item.Scan(encoding.Json[entity.BasicInfo](&basicItem)) + if err != nil { + return err + } + err = p.client.Del(ctx, basicItem.ID) + if err != nil { + return err + } + } + return nil +} diff --git a/gateway/apinto/service.go b/gateway/apinto/service.go new file mode 100644 index 00000000..c4d4f25b --- /dev/null +++ b/gateway/apinto/service.go @@ -0,0 +1,49 @@ +package apinto + +import ( + "context" + "fmt" + + "github.com/APIParkLab/APIPark/gateway" + admin_client "github.com/eolinker/eosc/process-admin/client" +) + +var _ gateway.IServiceClient = &ServiceClient{} + +type ServiceClient struct { + client admin_client.Client +} + +const KeyServiceOfApi = "service_of_api" + +func (s *ServiceClient) Online(ctx context.Context, resources ...*gateway.ServiceRelease) error { + s.client.Begin(ctx) + for _, r := range resources { + for _, api := range r.Apis { + err := s.client.HSet(ctx, fmt.Sprintf("%s:%s", KeyServiceOfApi, api), r.ID, "0") + if err != nil { + s.client.Rollback(ctx) + return err + } + } + } + return s.client.Commit(ctx) +} + +func (s *ServiceClient) Offline(ctx context.Context, resources ...*gateway.ServiceRelease) error { + s.client.Begin(ctx) + for _, r := range resources { + for _, api := range r.Apis { + err := s.client.HDel(ctx, fmt.Sprintf("%s:%s", KeyServiceOfApi, api), r.ID) + if err != nil { + s.client.Rollback(ctx) + return err + } + } + } + return s.client.Commit(ctx) +} + +func NewServiceClient(client admin_client.Client) *ServiceClient { + return &ServiceClient{client: client} +} diff --git a/gateway/apinto/subscribe.go b/gateway/apinto/subscribe.go new file mode 100644 index 00000000..b8c49a94 --- /dev/null +++ b/gateway/apinto/subscribe.go @@ -0,0 +1,48 @@ +package apinto + +import ( + "context" + "fmt" + + "github.com/APIParkLab/APIPark/gateway" + + admin_client "github.com/eolinker/eosc/process-admin/client" +) + +var _ gateway.ISubscribeClient = &SubscribeClient{} + +type SubscribeClient struct { + client admin_client.Client +} + +const KeySubscribeService = "subscription_service" + +func (s *SubscribeClient) Online(ctx context.Context, resources ...*gateway.SubscribeRelease) error { + s.client.Begin(ctx) + var err error + for _, r := range resources { + err = s.client.HSet(ctx, fmt.Sprintf("%s:%s", KeySubscribeService, r.Application), r.Service, r.Expired) + if err != nil { + s.client.Rollback(ctx) + return err + } + } + return s.client.Commit(ctx) +} + +func (s *SubscribeClient) Offline(ctx context.Context, resources ...*gateway.SubscribeRelease) error { + s.client.Begin(ctx) + var err error + for _, r := range resources { + err = s.client.HDel(ctx, fmt.Sprintf("%s:%s", KeySubscribeService, r.Application), r.Service) + if err != nil { + s.client.Rollback(ctx) + return err + } + } + return s.client.Commit(ctx) +} + +func NewSubscribeClient(client admin_client.Client) *SubscribeClient { + return &SubscribeClient{client: client} +} diff --git a/gateway/client.go b/gateway/client.go new file mode 100644 index 00000000..56b17cdc --- /dev/null +++ b/gateway/client.go @@ -0,0 +1,43 @@ +package gateway + +import "context" + +type ClientConfig struct { + // 请求地址列表 + Addresses []string + // 认证配置 + Auth *AuthConfig +} + +type AuthConfig struct { + // 认证类型 + Type string + // 认证信息 + Info map[string]interface{} +} + +type IClientDriver interface { + Project() IProjectClient + Application() IApplicationClient + Service() IServiceClient + Subscribe() ISubscribeClient + Dynamic(resource string) (IDynamicClient, error) + PluginSetting() IPluginSetting + Commit(ctx context.Context) error + Rollback(ctx context.Context) error + Begin(ctx context.Context) error + Close(ctx context.Context) error + // todo 插件同步 +} + +type IPluginSetting interface { + Init(ctx context.Context) error + Set(ctx context.Context, cfgs []*PluginConfig) error +} + +type PluginConfig struct { + Id string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Config map[string]interface{} `json:"config"` +} diff --git a/gateway/handler.go b/gateway/handler.go new file mode 100644 index 00000000..839953d2 --- /dev/null +++ b/gateway/handler.go @@ -0,0 +1,36 @@ +package gateway + +import ( + "context" +) + +var ( + initHandlers []InitHandler +) + +func RegisterInitHandler(handle InitHandler) { + initHandlers = append(initHandlers, handle) +} +func RegisterInitHandleFunc(handleFunc InitHandleFunc) { + initHandlers = append(initHandlers, handleFunc) +} + +type InitHandleFunc func(ctx context.Context, clusterId string, client IClientDriver) error + +func (f InitHandleFunc) Init(ctx context.Context, clusterId string, client IClientDriver) error { + return f(ctx, clusterId, client) +} + +type InitHandler interface { + Init(ctx context.Context, clusterId string, client IClientDriver) error +} + +func InitGateway(ctx context.Context, clusterId string, client IClientDriver) (err error) { + for _, h := range initHandlers { + err = h.Init(ctx, clusterId, client) + if err != nil { + return + } + } + return +} diff --git a/gateway/manager.go b/gateway/manager.go new file mode 100644 index 00000000..3b75e320 --- /dev/null +++ b/gateway/manager.go @@ -0,0 +1,59 @@ +package gateway + +import ( + "fmt" + + "github.com/eolinker/eosc" +) + +type IFactoryManager interface { + Set(name string, factory IFactory) + GetClient(name string, config *ClientConfig) (IClientDriver, error) + Drivers() []string +} + +var factoryManager = NewFactoryManager() + +func NewFactoryManager() IFactoryManager { + return &FactoryManager{factory: eosc.BuildUntyped[string, IFactory]()} +} + +type FactoryManager struct { + factory eosc.Untyped[string, IFactory] +} + +func (f *FactoryManager) Drivers() []string { + return f.factory.Keys() +} + +func (f *FactoryManager) Set(name string, factory IFactory) { + f.factory.Set(name, factory) +} + +func (f *FactoryManager) GetClient(name string, config *ClientConfig) (IClientDriver, error) { + factory, ok := f.factory.Get(name) + if !ok { + return nil, fmt.Errorf("client driver %s not found", name) + } + driver, err := factory.Create(config) + if err != nil { + return nil, fmt.Errorf("create client driver error: %w", err) + } + return driver, nil +} + +type IFactory interface { + Create(config *ClientConfig) (IClientDriver, error) +} + +func Register(name string, factory IFactory) { + factoryManager.Set(name, factory) +} + +func GetClient(name string, config *ClientConfig) (IClientDriver, error) { + return factoryManager.GetClient(name, config) +} + +func Drivers() []string { + return factoryManager.Drivers() +} diff --git a/gateway/resource.go b/gateway/resource.go new file mode 100644 index 00000000..ddafc3ac --- /dev/null +++ b/gateway/resource.go @@ -0,0 +1,139 @@ +package gateway + +import ( + "context" + "encoding/json" + "github.com/APIParkLab/APIPark/model/plugin_model" +) + +type IProjectClient IResourceClient[ProjectRelease] + +type IApplicationClient IResourceClient[ApplicationRelease] + +type IServiceClient IResourceClient[ServiceRelease] + +type ISubscribeClient IResourceClient[SubscribeRelease] + +type IResourceClient[T any] interface { + Online(ctx context.Context, resources ...*T) error + Offline(ctx context.Context, resources ...*T) error +} + +type IDynamicClient interface { + IResourceClient[DynamicRelease] + Versions(ctx context.Context, matchLabels map[string]string) (map[string]string, error) + Version(ctx context.Context, resourceId string) (string, error) +} + +type ProjectRelease struct { + Id string `json:"id"` + Version string `json:"version"` + Apis []*ApiRelease `json:"apis"` + Upstream *UpstreamRelease `json:"upstreams"` +} + +type ApiRelease struct { + *BasicItem + Path string + Method []string + Host []string + Plugins map[string]*Plugin + Service string + Rules []*MatchRule + Extends map[string]any + ProxyPath string + ProxyHeaders []*ProxyHeader + Retry int + Timeout int + Labels map[string]string +} + +type ProxyHeader struct { + Key string `json:"key"` + Value string `json:"value"` + Opt string `json:"opt"` +} + +type UpstreamRelease struct { + *BasicItem + Nodes []string + PassHost string + Scheme string + // Discovery 服务发现ID + Discovery string + // Service 服务发现服务名 + Service string + Balance string + Timeout int + Labels map[string]string +} + +type MatchRule struct { + Position string + MatchType string + Key string + Pattern string +} + +type Plugin struct { + Disable bool + Config plugin_model.ConfigType +} + +type BasicItem struct { + ID string + Description string + Version string + MatchLabels map[string]string +} + +type DynamicRelease struct { + *BasicItem + Attr map[string]interface{} +} + +func (d *DynamicRelease) UnmarshalJSON(bytes []byte) error { + basicInfo := new(BasicItem) + err := json.Unmarshal(bytes, basicInfo) + if err != nil { + return err + } + tmp := make(map[string]interface{}) + err = json.Unmarshal(bytes, &tmp) + if err != nil { + return err + } + d.BasicItem = basicInfo + d.Attr = tmp + return nil +} + +type ServiceRelease struct { + ID string + Apis []string +} + +type SubscribeRelease struct { + // 订阅服务ID + Service string + // 订阅方ID + Application string + // 过期时间 + Expired string +} + +type ApplicationRelease struct { + *BasicItem + Labels map[string]string + Authorizations []*Authorization +} + +type Authorization struct { + Type string + Position string + TokenName string + Expire int64 + Config map[string]interface{} + Label map[string]string + HideCredential bool +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..e644fc25 --- /dev/null +++ b/go.mod @@ -0,0 +1,69 @@ +module github.com/APIParkLab/APIPark + +go 1.21 + +//toolchain go1.21.1 + +require ( + github.com/eolinker/ap-account v1.0.6 + github.com/eolinker/eosc v0.17.3 + github.com/eolinker/go-common v1.0.1 + github.com/gabriel-vasile/mimetype v1.4.4 + github.com/gin-gonic/gin v1.10.0 + github.com/google/uuid v1.6.0 + github.com/urfave/cli/v2 v2.27.2 + golang.org/x/crypto v0.24.0 + gopkg.in/yaml.v3 v3.0.1 + gorm.io/gorm v1.25.5 +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/ghodss/yaml v1.0.0 // indirect + github.com/gin-contrib/gzip v1.0.1 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/kr/pretty v0.1.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/redis/go-redis/v9 v9.5.3 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.13 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.23.0 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gorm.io/driver/mysql v1.5.2 // indirect +) + +//replace github.com/eolinker/ap-account v1.0.4 => ../ap-account diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..3a935b8c --- /dev/null +++ b/go.sum @@ -0,0 +1,154 @@ +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eolinker/ap-account v1.0.6 h1:lzkmaItUoIguEKneUbP387qdgsUgjonKjHDnXsu6lTc= +github.com/eolinker/ap-account v1.0.6/go.mod h1:MViCOvUaS2QrVift1Be3yGjjMywzICL9317eOxoixSI= +github.com/eolinker/eosc v0.17.3 h1:sr2yT+v/AsqEdciRaaZZj0zL9pTufR5RvDW6+65hraQ= +github.com/eolinker/eosc v0.17.3/go.mod h1:xgq816hpanlMXFtZw7Ztdctb1eEk9UPHchY4NfFO6Cw= +github.com/eolinker/go-common v1.0.1 h1:Uan6QmXAlPiX6hc1ptSIHWvaWXNA+VlBjC4gCaDEiz0= +github.com/eolinker/go-common v1.0.1/go.mod h1:Kb/jENMN1mApnodvRgV4YwO9FJby1Jkt2EUjrBjvSX4= +github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= +github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE= +github.com/gin-contrib/gzip v1.0.1/go.mod h1:njt428fdUNRvjuJf16tZMYZ2Yl+WQB53X5wmhDwXvC4= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.5.3 h1:fOAp1/uJG+ZtcITgZOfYFmTKPE7n4Vclj1wZFgRciUU= +github.com/redis/go-redis/v9 v9.5.3/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI= +github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= +go.etcd.io/etcd/client/pkg/v3 v3.5.13 h1:RVZSAnWWWiI5IrYAXjQorajncORbS0zI48LQlE2kQWg= +go.etcd.io/etcd/client/pkg/v3 v3.5.13/go.mod h1:XxHT4u1qU12E2+po+UVPrEeL94Um6zL58ppuJWXSAB8= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY= +go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs= +gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8= +gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= +gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/init.go b/init.go new file mode 100644 index 00000000..f0edbabf --- /dev/null +++ b/init.go @@ -0,0 +1,13 @@ +package main + +import ( + _ "github.com/APIParkLab/APIPark/frontend" + _ "github.com/APIParkLab/APIPark/gateway/apinto" + _ "github.com/APIParkLab/APIPark/plugins/core" + _ "github.com/APIParkLab/APIPark/plugins/permit" + _ "github.com/APIParkLab/APIPark/plugins/publish_flow" + _ "github.com/eolinker/ap-account/plugin" + _ "github.com/eolinker/go-common/cache/cache_redis" + _ "github.com/eolinker/go-common/log-init" + _ "github.com/eolinker/go-common/store/store_mysql" +) diff --git a/initialization-none.go b/initialization-none.go new file mode 100644 index 00000000..f4c82193 --- /dev/null +++ b/initialization-none.go @@ -0,0 +1,13 @@ +//go:build !init + +package main + +import ( + _ "github.com/APIParkLab/APIPark/resources/access" + _ "github.com/APIParkLab/APIPark/resources/permit" + _ "github.com/APIParkLab/APIPark/resources/plugin" +) + +func doCheck() { + +} diff --git a/initialization.go b/initialization.go new file mode 100644 index 00000000..ae49f334 --- /dev/null +++ b/initialization.go @@ -0,0 +1,187 @@ +//go:build init + +package main + +import ( + "bytes" + "encoding/csv" + "fmt" + _ "github.com/APIParkLab/APIPark/resources/access" + _ "github.com/APIParkLab/APIPark/resources/permit" + _ "github.com/APIParkLab/APIPark/resources/plugin" + "github.com/eolinker/eosc/log" + "github.com/eolinker/go-common/access" + "github.com/eolinker/go-common/permit" + "github.com/eolinker/go-common/pm3" + "github.com/eolinker/go-common/utils" + "os" + "sort" + "strings" + "time" +) + +const unsetValue = "-" + +func doCheck() { + accessConf, unset := loadAccess() + + drivers := pm3.List() + + newAccess := 0 + for _, p := range drivers { + if ac, ok := p.(pm3.AccessConfig); ok { + if len(ac.Access()) > 0 { + for asKey := range ac.Access() { + key := strings.ToLower(asKey) + if _, has := accessConf[key]; !has { + accessConf[key] = empty(key) + newAccess++ + } + } + } + + } + + } + for asKey := range permit.All() { + key := strings.ToLower(asKey) + if _, has := accessConf[key]; !has { + accessConf[key] = empty(key) + newAccess++ + } + } + if newAccess > 0 || unset > 0 { + f := accessFile() + fmt.Printf("%d access need set, see : %s and %s", newAccess+unset, saveTemplate(accessConf, f), saveCsv(accessConf, f)) + + } + os.Exit(0) +} +func accessFile() string { + + if version == "" { + return time.Now().Format("20060102-150405") + } + return version +} +func saveCsv(as map[string]*Access, key string) string { + buf := &bytes.Buffer{} + w := csv.NewWriter(buf) + err := w.Write([]string{"group", "name", "cname", "desc"}) + if err != nil { + return "" + } + list := utils.MapToSliceNoKey(as) + sort.Sort(AccessListSort(list)) + for _, i := range list { + err := w.Write([]string{i.Group, i.Name, i.Cname, i.Desc}) + if err != nil { + return "" + } + } + w.Flush() + filePath := fmt.Sprintf("access.%s.csv", key) + err = os.WriteFile(filePath, buf.Bytes(), 0666) + if err != nil { + log.Fatal(err) + + } + return filePath +} + +type AccessListSort []*Access + +func (ls AccessListSort) Len() int { + return len(ls) +} + +func (ls AccessListSort) Less(i, j int) bool { + if ls[i].Group != ls[j].Group { + return ls[i].Group < ls[j].Group + } + if ls[i].Cname != ls[j].Cname { + return ls[i].Cname < ls[j].Cname + } + return ls[i].Name < ls[j].Name +} + +func (ls AccessListSort) Swap(i, j int) { + ls[i], ls[j] = ls[j], ls[i] +} + +func saveTemplate(as map[string]*Access, key string) string { + out := make(map[string][]access.Access) + + for _, a := range as { + + out[a.Group] = append(out[a.Group], access.Access{ + Name: a.Name, + CName: a.Cname, + Desc: a.Desc, + }) + } + buf := &bytes.Buffer{} + err := yaml.NewEncoder(buf).Encode(out) + if err != nil { + log.Fatal(err) + return "" + } + filePath := fmt.Sprintf("access.%s.yml", key) + err = os.WriteFile(filePath, buf.Bytes(), 0666) + if err != nil { + log.Fatal(err) + + } + return filePath +} + +type Access struct { + Key string + Group string + Name string + Cname string + Desc string +} + +func empty(key string) *Access { + group, name := readKey(key) + return &Access{ + Key: key, + Group: group, + Name: name, + Cname: unsetValue, + Desc: unsetValue, + } +} +func readKey(key string) (group string, name string) { + ls := strings.Split(key, ".") + if len(ls) != 2 { + log.Fatal("invalid access key:[%s]", key) + } + return ls[0], ls[1] +} + +func loadAccess() (map[string]*Access, int) { + confAccess := access.All() + unset := 0 + as := make(map[string]*Access) + for group, al := range confAccess { + for _, a := range al { + g, name := readKey(a.Name) + if g != group { + log.Fatal("invalid access key:[%s]", a.Name) + } + as[a.Name] = &Access{ + Key: a.Name, + Group: group, + Name: name, + Cname: a.CName, + Desc: a.Desc, + } + if a.CName == unsetValue || a.Desc == unsetValue { + unset++ + } + } + } + return as, unset +} diff --git a/main.go b/main.go new file mode 100644 index 00000000..872a3616 --- /dev/null +++ b/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "flag" + "fmt" + "github.com/eolinker/eosc/log" + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/cftool" + "github.com/eolinker/go-common/permit" + "github.com/eolinker/go-common/server" + "net" + "net/http" +) + +var ( + version string + confPath string +) + +func init() { + flag.StringVar(&confPath, "c", "config.yml", "`config` file path for server ") +} + +type ServerConfig struct { + Port int `yaml:"port"` +} + +func main() { + doCheck() + + flag.Parse() + + cf := new(ServerConfig) + cftool.Register(fmt.Sprintf("root:%s", confPath), cf) + cftool.ReadFile(confPath) + + ser := server.CreateServer() + + err := autowire.CheckComplete() + if err != nil { + log.Fatal("check autowired:", err) + return + } + + if cf.Port == 0 { + log.Fatal("need port") + } + ln, err := net.Listen("tcp", fmt.Sprintf(":%d", cf.Port)) + if err != nil { + log.Fatal(err) + return + } + srv := ser.Build() + for access, paths := range srv.Permits() { + permit.AddPermitRule(access, paths...) + } + + err = http.Serve(ln, srv) + if err != nil { + log.Fatal(err) + return + } + +} diff --git a/middleware/permit/identity/identity.go b/middleware/permit/identity/identity.go new file mode 100644 index 00000000..ab11078d --- /dev/null +++ b/middleware/permit/identity/identity.go @@ -0,0 +1,22 @@ +package permit_identity + +import ( + "context" +) + +const ( + SystemGroup = "system" + TeamGroup = "team" + ProjectGroup = "project" +) + +type IdentityTeamService interface { + IdentifyTeam(ctx context.Context, team string, uid string) ([]string, error) +} + +// type IdentityProjectService interface { +// IdentifyProject(ctx context.Context, project string, uid string) ([]string, error) +// } +type IdentitySystemService interface { + IdentifySystem(ctx context.Context, uid string) ([]string, error) +} diff --git a/middleware/permit/identity/iml.go b/middleware/permit/identity/iml.go new file mode 100644 index 00000000..5893cb64 --- /dev/null +++ b/middleware/permit/identity/iml.go @@ -0,0 +1 @@ +package permit_identity diff --git a/middleware/permit/permit.go b/middleware/permit/permit.go new file mode 100644 index 00000000..a82574e9 --- /dev/null +++ b/middleware/permit/permit.go @@ -0,0 +1,97 @@ +package permit_middleware + +import ( + "net/http" + "reflect" + + permit_identity "github.com/APIParkLab/APIPark/middleware/permit/identity" + "github.com/eolinker/eosc/log" + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/permit" + "github.com/eolinker/go-common/pm3" + "github.com/eolinker/go-common/utils" + "github.com/gin-gonic/gin" +) + +var ( + checkSort = []string{permit_identity.TeamGroup, permit_identity.SystemGroup} +) + +type IPermitMiddleware interface { + pm3.IMiddleware +} + +func init() { + autowire.Auto[IPermitMiddleware](func() reflect.Value { + return reflect.ValueOf(new(PermitMiddleware)) + }) +} + +var ( + _ IPermitMiddleware = (*PermitMiddleware)(nil) +) + +type PermitMiddleware struct { + permitService permit.IPermit `autowired:""` +} + +func (p *PermitMiddleware) Sort() int { + return 99 +} + +func (p *PermitMiddleware) Check(method string, path string) (bool, []gin.HandlerFunc) { + // 当前路径是否有配置权限 + accessRules, has := permit.GetPathRule(method, path) + + if !has || len(accessRules) == 0 { + return false, nil + } + + return true, []gin.HandlerFunc{ + func(ginCtx *gin.Context) { + userId := utils.UserId(ginCtx) + if userId == "" { + ginCtx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": http.StatusForbidden, "msg": "not login", "success": "fail"}) + ginCtx.Abort() + return + } + + //if userId == "admin" { + // // 超级管理员不校验 + // return + //} + + for _, group := range checkSort { + accessList, has := accessRules[group] + if !has { + // 当前分组没有配置权限 + continue + } + domainHandler, has := permit.SelectDomain(group) + if !has { + // 当前分组没有配置身份handler + continue + } + _, myAccess, ok := domainHandler(ginCtx) + if !ok { + continue + } + accessMap := utils.SliceToMapO(myAccess, func(s string) (string, struct{}) { + return s, struct{}{} + }) + for _, acc := range accessList { + if _, ok := accessMap[acc]; ok { + return + } + } + } + //所有group都校验不通过 + log.DebugF("no permission:%s", ginCtx.FullPath()) + ginCtx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": http.StatusForbidden, "msg": "no permission", "success": "fail"}) + }, + } +} + +func (p *PermitMiddleware) Name() string { + return "permit" +} diff --git a/model/plugin_model/config.go b/model/plugin_model/config.go new file mode 100644 index 00000000..eaea35b5 --- /dev/null +++ b/model/plugin_model/config.go @@ -0,0 +1,11 @@ +package plugin_model + +type PluginConfig struct { + Name string `json:"name"` + Status Status `json:"status"` + Config ConfigType `json:"config,omitempty"` +} + +type ConfigType map[string]any + +type Render map[string]any diff --git a/model/plugin_model/define.go b/model/plugin_model/define.go new file mode 100644 index 00000000..9236c577 --- /dev/null +++ b/model/plugin_model/define.go @@ -0,0 +1,12 @@ +package plugin_model + +type Define struct { + Extend string + Name string + Cname string + Desc string + Kind Kind + Status Status + Config ConfigType + Render Render +} diff --git a/model/plugin_model/type.go b/model/plugin_model/type.go new file mode 100644 index 00000000..a95cce28 --- /dev/null +++ b/model/plugin_model/type.go @@ -0,0 +1,174 @@ +package plugin_model + +import ( + "database/sql/driver" + "encoding/json" +) + +type Kind int + +func (k *Kind) UnmarshalJSON(bytes []byte) error { + str := "" + err := json.Unmarshal(bytes, &str) + if err != nil { + return err + } + *k = ParseKind(str) + return nil +} + +func (k *Kind) MarshalJSON() ([]byte, error) { + return json.Marshal(k.String()) +} + +func (k *Kind) String() string { + switch *k { + case InnerKind: + return "inner" + + case OpenKind: + return "global" + default: + return "unknown" + + } +} + +func (k *Kind) Scan(src any) error { + switch v := src.(type) { + case string: + *k = ParseKind(v) + case []byte: + *k = ParseKind(string(v)) + case int: + *k = Kind(v) + case int64: + *k = Kind(v) + case int32: + *k = Kind(v) + case int16: + *k = Kind(v) + case int8: + *k = Kind(v) + case uint: + *k = Kind(v) + case uint64: + *k = Kind(v) + case uint32: + *k = Kind(v) + case uint16: + *k = Kind(v) + case uint8: + *k = Kind(v) + + default: + *k = OpenKind + } + return nil +} + +func ParseKind(string2 string) Kind { + switch string2 { + case "inner", "0": + return InnerKind + case "global", "1": + return OpenKind + default: + return OpenKind + } +} + +func (k *Kind) Value() (driver.Value, error) { + if *k == unKnownKind { + return OpenKind, nil + } + return *k, nil +} + +const ( + InnerKind Kind = iota + OpenKind + unKnownKind +) +const ( + Enable Status = iota + Global + Disable + unKnownStatus +) + +type Status int + +func (s *Status) UnmarshalJSON(bytes []byte) error { + str := "" + err := json.Unmarshal(bytes, &str) + if err != nil { + return err + } + *s = ParseStatus(str) + return nil +} + +func (s *Status) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} + +func (s *Status) Scan(src any) error { + switch v := src.(type) { + case string: + *s = ParseStatus(v) + case []byte: + *s = ParseStatus(string(v)) + case int: + *s = Status(v) + case int64: + *s = Status(v) + case int32: + *s = Status(v) + case int16: + *s = Status(v) + case int8: + *s = Status(v) + case uint: + *s = Status(v) + case uint64: + *s = Status(v) + case uint32: + *s = Status(v) + case uint16: + *s = Status(v) + case uint8: + *s = Status(v) + default: + *s = Enable + + } + return nil +} + +func (s *Status) String() string { + switch *s { + case Enable: + return "enable" + case Global: + return "global" + case Disable: + return "disable" + default: + return "unknown" + } + +} + +func ParseStatus(string2 string) Status { + switch string2 { + case "enable", "0": + return Enable + case "disable", "2": + return Disable + case "global", "1": + return Global + default: + return unKnownStatus + } +} diff --git a/module/api/api.go b/module/api/api.go new file mode 100644 index 00000000..b66d8fe6 --- /dev/null +++ b/module/api/api.go @@ -0,0 +1,42 @@ +package api + +import ( + "context" + "reflect" + + "github.com/eolinker/go-common/autowire" + + api_dto "github.com/APIParkLab/APIPark/module/api/dto" +) + +type IApiModule interface { + // Detail 获取API详情 + Detail(ctx context.Context, serviceId string, apiId string) (*api_dto.ApiDetail, error) + // SimpleDetail 获取API简要详情 + SimpleDetail(ctx context.Context, serviceId string, apiId string) (*api_dto.ApiSimpleDetail, error) + // Search 获取API列表 + Search(ctx context.Context, keyword string, serviceId string) ([]*api_dto.ApiItem, error) + // SimpleSearch 获取API简要列表 + SimpleSearch(ctx context.Context, keyword string, serviceId string) ([]*api_dto.ApiSimpleItem, error) + SimpleList(ctx context.Context, serviceId string) ([]*api_dto.ApiSimpleItem, error) + // Create 创建API + Create(ctx context.Context, serviceId string, dto *api_dto.CreateApi) (*api_dto.ApiSimpleDetail, error) + // Edit 编辑API + Edit(ctx context.Context, serviceId string, apiId string, dto *api_dto.EditApi) (*api_dto.ApiSimpleDetail, error) + // Delete 删除API + Delete(ctx context.Context, serviceId string, apiId string) error + // Copy 复制API + Copy(ctx context.Context, serviceId string, apiId string, dto *api_dto.CreateApi) (*api_dto.ApiSimpleDetail, error) + // ApiDocDetail 获取API文档详情 + ApiDocDetail(ctx context.Context, serviceId string, apiId string) (*api_dto.ApiDocDetail, error) + // ApiProxyDetail 获取API代理详情 + ApiProxyDetail(ctx context.Context, serviceId string, apiId string) (*api_dto.ApiProxyDetail, error) + // Prefix 获取API前缀 + Prefix(ctx context.Context, serviceId string) (string, error) +} + +func init() { + autowire.Auto[IApiModule](func() reflect.Value { + return reflect.ValueOf(new(imlApiModule)) + }) +} diff --git a/module/api/dto/input.go b/module/api/dto/input.go new file mode 100644 index 00000000..260bff2a --- /dev/null +++ b/module/api/dto/input.go @@ -0,0 +1,127 @@ +package api_dto + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/eolinker/go-common/utils" + "strings" + + "github.com/APIParkLab/APIPark/service/api" +) + +var validMethods = map[string]struct{}{ + "GET": {}, + "POST": {}, + "PUT": {}, + "DELETE": {}, + "PATCH": {}, + "HEAD": {}, + "OPTIONS": {}, +} + +type CreateApi struct { + Id string `json:"id"` + Name string `json:"name"` + Path string `json:"path"` + Method string `json:"method"` + Description string `json:"description"` + MatchRules []Match `json:"match"` + Proxy *InputProxy `json:"proxy"` +} + +type InputProxy struct { + Path string `json:"path"` + //Upstream string `json:"upstream" aocheck:"upstream"` + Timeout int `json:"timeout"` + Retry int `json:"retry"` + Headers []*Header `json:"headers"` + Extends map[string]any `json:"extends"` + Plugins map[string]api.PluginSetting `json:"plugins"` +} +type Match struct { + Position string `json:"position"` + MatchType string `json:"match_type"` + Key string `json:"key"` + Pattern string `json:"pattern"` +} + +func (a *CreateApi) Validate() error { + if a.Id == "" { + return errors.New("id is null") + } + if a.Name == "" { + return errors.New("name is null") + } + a.Path = fmt.Sprintf("/%s", strings.TrimPrefix(a.Path, "/")) + a.Method = strings.ToUpper(a.Method) + if _, ok := validMethods[a.Method]; !ok { + return fmt.Errorf("method(%s) is invalid", a.Method) + } + return nil +} + +func (a *CreateApi) ToServiceRouter() *api.Router { + router := &api.Router{ + Method: a.Method, + Path: a.Path, + } + for _, match := range a.MatchRules { + router.MatchRules = append(router.MatchRules, &api.Match{ + Position: match.Position, + MatchType: match.MatchType, + Key: match.Key, + Pattern: match.Pattern, + }) + } + return router +} + +type EditApi struct { + Info struct { + Name *string `json:"name"` + Description *string `json:"description"` + } `json:"info"` + Proxy *InputProxy `json:"proxy"` + Doc *map[string]interface{} `json:"doc"` +} + +func ToServiceProxy(proxy *InputProxy) *api.Proxy { + if proxy == nil { + return &api.Proxy{} + } + headers := utils.SliceToSlice(proxy.Headers, func(h *Header) *api.Header { + return &api.Header{ + Key: h.Key, + Value: h.Value, + Opt: h.Opt, + } + }) + + return &api.Proxy{ + Path: proxy.Path, + //Upstream: proxy.Upstream, + Timeout: proxy.Timeout, + Retry: proxy.Retry, + Extends: proxy.Extends, + Plugins: proxy.Plugins, + Headers: headers, + } +} + +func ToServiceDocument(doc map[string]interface{}) *api.Document { + if doc == nil { + return &api.Document{ + Content: "{}", + } + } + content, _ := json.Marshal(doc) + + return &api.Document{ + Content: string(content), + } +} + +type ListInput struct { + Projects []string `json:"projects"` +} diff --git a/module/api/dto/output.go b/module/api/dto/output.go new file mode 100644 index 00000000..1787e039 --- /dev/null +++ b/module/api/dto/output.go @@ -0,0 +1,115 @@ +package api_dto + +import ( + "encoding/json" + + "github.com/eolinker/go-common/utils" + + "github.com/APIParkLab/APIPark/service/api" + "github.com/eolinker/go-common/auto" +) + +type ApiItem struct { + Id string `json:"id"` + Name string `json:"name"` + Method string `json:"method"` + Path string `json:"request_path"` + Creator auto.Label `json:"creator" aolabel:"user"` + Updater auto.Label `json:"updater" aolabel:"user"` + CreateTime auto.TimeLabel `json:"create_time"` + UpdateTime auto.TimeLabel `json:"update_time"` + CanDelete bool `json:"can_delete"` +} + +type ApiSimpleItem struct { + Id string `json:"id"` + Name string `json:"name"` + Method string `json:"method"` + Path string `json:"request_path"` +} + +type ApiDetail struct { + ApiSimpleDetail + Proxy *Proxy `json:"proxy"` + Doc map[string]interface{} `json:"doc"` +} + +func GenApiSimpleDetail(api *api.Info) *ApiSimpleDetail { + match := make([]Match, 0) + if api.Match == "" { + api.Match = "[]" + } + json.Unmarshal([]byte(api.Match), &match) + + return &ApiSimpleDetail{ + Id: api.UUID, + Name: api.Name, + Description: api.Description, + Method: api.Method, + Path: api.Path, + MatchRules: match, + Creator: auto.UUID(api.Creator), + Updater: auto.UUID(api.Updater), + CreateTime: auto.TimeLabel(api.CreateAt), + UpdateTime: auto.TimeLabel(api.UpdateAt), + } +} + +type ApiSimpleDetail struct { + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Method string `json:"method"` + Path string `json:"path"` + MatchRules []Match `json:"match"` + Creator auto.Label `json:"creator" aolabel:"user"` + Updater auto.Label `json:"updater" aolabel:"user"` + CreateTime auto.TimeLabel `json:"create_time"` + UpdateTime auto.TimeLabel `json:"update_time"` +} + +type ApiDocDetail struct { + ApiSimpleDetail + Doc map[string]interface{} `json:"doc"` +} + +type ApiProxyDetail struct { + ApiSimpleDetail + Proxy *Proxy `json:"proxy"` +} + +func FromServiceProxy(proxy *api.Proxy) *Proxy { + if proxy == nil { + return nil + } + + return &Proxy{ + Path: proxy.Path, + Timeout: proxy.Timeout, + Retry: proxy.Retry, + Headers: utils.SliceToSlice(proxy.Headers, func(header *api.Header) *Header { + return &Header{ + Key: header.Key, + Value: header.Value, + Opt: header.Opt, + } + }), + Extends: proxy.Extends, + Plugins: proxy.Plugins, + } +} + +type Proxy struct { + Path string `json:"path"` + Timeout int `json:"timeout"` + Retry int `json:"retry"` + Headers []*Header `json:"headers"` + Extends map[string]any `json:"extends"` + Plugins map[string]api.PluginSetting `json:"plugins"` +} + +type Header struct { + Key string `json:"key"` + Value string `json:"value"` + Opt string `json:"opt"` +} diff --git a/module/api/iml.go b/module/api/iml.go new file mode 100644 index 00000000..09215cfd --- /dev/null +++ b/module/api/iml.go @@ -0,0 +1,451 @@ +package api + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/APIParkLab/APIPark/service/service" + "github.com/APIParkLab/APIPark/service/upstream" + + "gorm.io/gorm" + + "github.com/APIParkLab/APIPark/service/team" + + "github.com/google/uuid" + + "github.com/eolinker/go-common/auto" + "github.com/eolinker/go-common/utils" + + "github.com/eolinker/go-common/store" + + "github.com/APIParkLab/APIPark/service/api" + + api_dto "github.com/APIParkLab/APIPark/module/api/dto" +) + +var _ IApiModule = (*imlApiModule)(nil) +var ( + asServer = map[string]bool{ + "as_server": true, + } +) + +type imlApiModule struct { + teamService team.ITeamService `autowired:""` + serviceService service.IServiceService `autowired:""` + apiService api.IAPIService `autowired:""` + upstreamService upstream.IUpstreamService `autowired:""` + transaction store.ITransaction `autowired:""` +} + +func (i *imlApiModule) SimpleList(ctx context.Context, serviceId string) ([]*api_dto.ApiSimpleItem, error) { + + list, err := i.apiService.ListForService(ctx, serviceId) + apiInfos, err := i.apiService.ListInfo(ctx, utils.SliceToSlice(list, func(s *api.API) string { + return s.UUID + })...) + if err != nil { + return nil, err + } + + out := utils.SliceToSlice(apiInfos, func(item *api.Info) *api_dto.ApiSimpleItem { + return &api_dto.ApiSimpleItem{ + Id: item.UUID, + Name: item.Name, + Method: item.Method, + Path: item.Path, + } + }) + return out, nil +} + +func (i *imlApiModule) Detail(ctx context.Context, serviceId string, apiId string) (*api_dto.ApiDetail, error) { + _, err := i.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return nil, err + } + + detail, err := i.apiService.GetInfo(ctx, apiId) + if err != nil { + return nil, err + } + + apiDetail := &api_dto.ApiDetail{ + ApiSimpleDetail: *api_dto.GenApiSimpleDetail(detail), + } + proxy, err := i.apiService.LatestProxy(ctx, apiId) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + } + if proxy != nil { + + apiDetail.Proxy = api_dto.FromServiceProxy(proxy.Data) + } + + document, err := i.apiService.LatestDocument(ctx, apiId) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + } + if document != nil { + doc := make(map[string]interface{}) + err = json.Unmarshal([]byte(document.Data.Content), &doc) + if err != nil { + return nil, err + } + apiDetail.Doc = doc + } + + return apiDetail, nil +} + +func (i *imlApiModule) SimpleDetail(ctx context.Context, serviceId string, apiId string) (*api_dto.ApiSimpleDetail, error) { + _, err := i.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return nil, err + } + + detail, err := i.apiService.GetInfo(ctx, apiId) + if err != nil { + return nil, err + } + + return api_dto.GenApiSimpleDetail(detail), nil +} + +func (i *imlApiModule) Search(ctx context.Context, keyword string, serviceId string) ([]*api_dto.ApiItem, error) { + _, err := i.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return nil, err + } + + list, err := i.apiService.Search(ctx, keyword, map[string]interface{}{ + "service": serviceId, + }) + if err != nil { + return nil, err + } + apiInfos, err := i.apiService.ListInfo(ctx, utils.SliceToSlice(list, func(s *api.API) string { + return s.UUID + })...) + if err != nil { + return nil, err + } + utils.Sort(apiInfos, func(a, b *api.Info) bool { + return a.UpdateAt.After(b.UpdateAt) + }) + out := utils.SliceToSlice(apiInfos, func(item *api.Info) *api_dto.ApiItem { + return &api_dto.ApiItem{ + Id: item.UUID, + Name: item.Name, + Method: item.Method, + Path: item.Path, + Creator: auto.UUID(item.Creator), + Updater: auto.UUID(item.Updater), + CreateTime: auto.TimeLabel(item.CreateAt), + UpdateTime: auto.TimeLabel(item.UpdateAt), + CanDelete: true, + } + }) + + return out, nil +} + +func (i *imlApiModule) SimpleSearch(ctx context.Context, keyword string, serviceId string) ([]*api_dto.ApiSimpleItem, error) { + _, err := i.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return nil, err + } + + list, err := i.apiService.Search(ctx, keyword, map[string]interface{}{ + "service": serviceId, + }) + if err != nil { + return nil, err + } + apiInfos, err := i.apiService.ListInfo(ctx, utils.SliceToSlice(list, func(s *api.API) string { + return s.UUID + })...) + if err != nil { + return nil, err + } + out := utils.SliceToSlice(apiInfos, func(item *api.Info) *api_dto.ApiSimpleItem { + return &api_dto.ApiSimpleItem{ + Id: item.UUID, + Name: item.Name, + Method: item.Method, + Path: item.Path, + } + }) + return out, nil +} + +func (i *imlApiModule) Create(ctx context.Context, serviceId string, dto *api_dto.CreateApi) (*api_dto.ApiSimpleDetail, error) { + info, err := i.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return nil, err + } + prefix, err := i.Prefix(ctx, serviceId) + if err != nil { + return nil, err + } + err = i.transaction.Transaction(ctx, func(ctx context.Context) error { + if dto.Id == "" { + dto.Id = uuid.New().String() + } + err = dto.Validate() + if err != nil { + return err + } + + path := fmt.Sprintf("%s%s", prefix, dto.Path) + err = i.apiService.Exist(ctx, "", &api.ExistAPI{Path: dto.Path, Method: dto.Method}) + if err != nil { + return fmt.Errorf("api path %s,method: %s already exist", dto.Path, dto.Method) + } + proxy := api_dto.ToServiceProxy(dto.Proxy) + err = i.apiService.SaveProxy(ctx, dto.Id, proxy) + if err != nil { + return err + } + err = i.apiService.SaveDocument(ctx, dto.Id, api_dto.ToServiceDocument(nil)) + if err != nil { + return err + } + + match, _ := json.Marshal(dto.MatchRules) + return i.apiService.Create(ctx, &api.CreateAPI{ + UUID: dto.Id, + Name: dto.Name, + Description: dto.Description, + Service: serviceId, + Team: info.Team, + Method: dto.Method, + Path: path, + Match: string(match), + //Upstream: proxy.Upstream, + }) + }) + if err != nil { + return nil, err + } + return i.SimpleDetail(ctx, serviceId, dto.Id) +} + +func (i *imlApiModule) Edit(ctx context.Context, serviceId string, apiId string, dto *api_dto.EditApi) (*api_dto.ApiSimpleDetail, error) { + _, err := i.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return nil, err + } + + err = i.transaction.Transaction(ctx, func(ctx context.Context) error { + var up *string + if dto.Proxy != nil { + err = i.apiService.SaveProxy(ctx, apiId, api_dto.ToServiceProxy(dto.Proxy)) + if err != nil { + return err + } + //if dto.Proxy.Upstream != "" { + // up = &dto.Proxy.Upstream + //} + } + err = i.apiService.Save(ctx, apiId, &api.EditAPI{ + Name: dto.Info.Name, + Description: dto.Info.Description, + Upstream: up, + }) + if err != nil { + return err + } + + if dto.Doc != nil { + err = i.apiService.SaveDocument(ctx, apiId, api_dto.ToServiceDocument(*dto.Doc)) + if err != nil { + return err + } + } + return nil + + }) + if err != nil { + return nil, err + } + return i.SimpleDetail(ctx, serviceId, apiId) +} + +func (i *imlApiModule) Delete(ctx context.Context, serviceId string, apiId string) error { + _, err := i.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return err + } + return i.apiService.Delete(ctx, apiId) +} + +func (i *imlApiModule) Copy(ctx context.Context, serviceId string, apiId string, dto *api_dto.CreateApi) (*api_dto.ApiSimpleDetail, error) { + info, err := i.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return nil, err + } + oldApi, err := i.apiService.Get(ctx, apiId) + if err != nil { + return nil, err + } + prefix, err := i.Prefix(ctx, serviceId) + if err != nil { + return nil, err + } + err = i.transaction.Transaction(ctx, func(ctx context.Context) error { + if dto.Id == "" { + dto.Id = uuid.New().String() + } + err = dto.Validate() + if err != nil { + return err + } + + path := fmt.Sprintf("%s/%s", strings.TrimSuffix(prefix, "/"), strings.TrimPrefix(dto.Path, "/")) + err = i.apiService.Exist(ctx, serviceId, &api.ExistAPI{Path: path, Method: dto.Method}) + if err != nil { + return err + } + + proxy, err := i.apiService.LatestProxy(ctx, oldApi.UUID) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + } + //upstreamId := "" + if proxy != nil { + err = i.apiService.SaveProxy(ctx, dto.Id, proxy.Data) + if err != nil { + return err + } + //upstreamId = proxy.Data.Upstream + } + + doc, err := i.apiService.LatestDocument(ctx, oldApi.UUID) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + } + if doc != nil { + err = i.apiService.SaveDocument(ctx, dto.Id, doc.Data) + if err != nil { + return err + } + } + match, _ := json.Marshal(dto.MatchRules) + return i.apiService.Create(ctx, &api.CreateAPI{ + UUID: dto.Id, + Name: dto.Name, + Service: serviceId, + Team: info.Team, + Method: dto.Method, + Path: path, + Match: string(match), + //Upstream: upstreamId, + }) + + }) + if err != nil { + return nil, err + } + return i.SimpleDetail(ctx, serviceId, dto.Id) +} + +func (i *imlApiModule) ApiDocDetail(ctx context.Context, serviceId string, apiId string) (*api_dto.ApiDocDetail, error) { + _, err := i.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return nil, err + } + + apiBase, err := i.apiService.Get(ctx, apiId) + if err != nil { + return nil, err + } + if apiBase.IsDelete { + return nil, errors.New("api is delete") + } + + detail, err := i.apiService.GetInfo(ctx, apiBase.UUID) + if err != nil { + return nil, err + } + document, err := i.apiService.LatestDocument(ctx, apiId) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + } + var doc map[string]interface{} + if document != nil { + doc = make(map[string]interface{}) + err = json.Unmarshal([]byte(document.Data.Content), &doc) + if err != nil { + return nil, err + } + } + return &api_dto.ApiDocDetail{ + ApiSimpleDetail: *api_dto.GenApiSimpleDetail(detail), + Doc: doc, + }, nil +} + +func (i *imlApiModule) ApiProxyDetail(ctx context.Context, serviceId string, apiId string) (*api_dto.ApiProxyDetail, error) { + _, err := i.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return nil, err + } + apiBase, err := i.apiService.Get(ctx, apiId) + if err != nil { + return nil, err + } + if apiBase.IsDelete { + return nil, errors.New("api is delete") + } + if apiBase.Service != serviceId { + return nil, errors.New("api is not in project") + } + + detail, err := i.apiService.GetInfo(ctx, apiId) + if err != nil { + return nil, err + } + + apiDetail := &api_dto.ApiProxyDetail{ + ApiSimpleDetail: *api_dto.GenApiSimpleDetail(detail), + } + proxy, err := i.apiService.LatestProxy(ctx, apiId) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + } + if proxy != nil { + apiDetail.Proxy = api_dto.FromServiceProxy(proxy.Data) + } + return apiDetail, nil + +} + +func (i *imlApiModule) Prefix(ctx context.Context, serviceId string) (string, error) { + pInfo, err := i.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return "", err + } + + if pInfo.Prefix != "" { + if pInfo.Prefix[0] != '/' { + pInfo.Prefix = fmt.Sprintf("/%s", strings.TrimSuffix(pInfo.Prefix, "/")) + } + } + return strings.TrimSuffix(pInfo.Prefix, "/"), nil +} diff --git a/module/application-authorization/auth-driver/aksk/aksk.go b/module/application-authorization/auth-driver/aksk/aksk.go new file mode 100644 index 00000000..083b9ffa --- /dev/null +++ b/module/application-authorization/auth-driver/aksk/aksk.go @@ -0,0 +1,54 @@ +package aksk + +import ( + "encoding/json" + "fmt" + + auth_driver "github.com/APIParkLab/APIPark/module/application-authorization/auth-driver" + + application_authorization_dto "github.com/APIParkLab/APIPark/module/application-authorization/dto" +) + +var _ auth_driver.IAuthConfig = &Config{} + +const ( + driver = "aksk" +) + +func init() { + auth_driver.RegisterAuthFactory(driver, auth_driver.NewFactory[Config](driver)) +} + +type Config struct { + Ak string `json:"ak"` + Sk string `json:"sk"` + Label map[string]string `json:"label"` +} + +func (a *Config) ID() string { + //TODO implement me + panic("implement me") +} + +func (a *Config) Valid() ([]byte, error) { + if a.Ak == "" { + return nil, fmt.Errorf("access key is empty") + } + if a.Sk == "" { + return nil, fmt.Errorf("secret key is empty") + } + return json.Marshal(a) +} + +func (a *Config) Detail() []application_authorization_dto.DetailItem { + return []application_authorization_dto.DetailItem{ + { + Key: "Access Key", + Value: a.Ak, + }, + { + Key: "Secret Key", + Value: a.Sk, + }, + } +} diff --git a/module/application-authorization/auth-driver/apikey/apikey.go b/module/application-authorization/auth-driver/apikey/apikey.go new file mode 100644 index 00000000..ba319c87 --- /dev/null +++ b/module/application-authorization/auth-driver/apikey/apikey.go @@ -0,0 +1,47 @@ +package apikey + +import ( + "encoding/json" + "fmt" + + auth_driver "github.com/APIParkLab/APIPark/module/application-authorization/auth-driver" + + "github.com/eolinker/go-common/utils" + + application_authorization_dto "github.com/APIParkLab/APIPark/module/application-authorization/dto" +) + +const ( + driver = "apikey" +) + +func init() { + auth_driver.RegisterAuthFactory(driver, auth_driver.NewFactory[Config](driver)) +} + +var _ auth_driver.IAuthConfig = (*Config)(nil) + +type Config struct { + Apikey string `json:"apikey"` + Label map[string]string `json:"label"` +} + +func (a *Config) ID() string { + return utils.Md5(a.Apikey) +} + +func (a *Config) Valid() ([]byte, error) { + if a.Apikey == "" { + return nil, fmt.Errorf("apikey is empty") + } + return json.Marshal(a) +} + +func (a *Config) Detail() []application_authorization_dto.DetailItem { + return []application_authorization_dto.DetailItem{ + { + Key: "Apikey", + Value: a.Apikey, + }, + } +} diff --git a/module/application-authorization/auth-driver/auth.go b/module/application-authorization/auth-driver/auth.go new file mode 100644 index 00000000..5d454ff3 --- /dev/null +++ b/module/application-authorization/auth-driver/auth.go @@ -0,0 +1,88 @@ +package auth_driver + +import ( + "encoding/json" + "fmt" + + application_authorization_dto "github.com/APIParkLab/APIPark/module/application-authorization/dto" +) + +type IAuth interface { + GenerateID(position string, tokenName string) string + Type() string + AuthConfig() IAuthConfig +} + +type IAuthConfig interface { + ID() string + Valid() ([]byte, error) + Detail() []application_authorization_dto.DetailItem +} + +type Auth struct { + driver string + authConfig IAuthConfig +} + +func (a *Auth) GenerateID(position string, tokenName string) string { + return fmt.Sprintf("%s-%s-%s-%s", position, tokenName, a.driver, a.authConfig.ID()) +} + +func (a *Auth) Type() string { + return a.driver +} + +func (a *Auth) AuthConfig() IAuthConfig { + return a.authConfig +} + +type IFactory interface { + Create(config interface{}) (IAuth, error) +} + +type Factory[T any] struct { + driver string +} + +func NewFactory[T any](driver string) *Factory[T] { + return &Factory[T]{driver: driver} +} + +func (f *Factory[T]) Create(config interface{}) (IAuth, error) { + cfg, err := generateStruct[T](config) + if err != nil { + return nil, err + } + authConfig, ok := interface{}(cfg).(IAuthConfig) + if !ok { + return nil, fmt.Errorf("no implement IAuthConfig") + } + return &Auth{driver: f.driver, authConfig: authConfig}, nil +} + +func generateStruct[T any](cfg interface{}) (*T, error) { + result := new(T) + switch c := cfg.(type) { + case string: + err := json.Unmarshal([]byte(c), result) + if err != nil { + return nil, err + } + case []byte: + err := json.Unmarshal(c, result) + if err != nil { + return nil, err + } + case *map[string]interface{}, map[string]interface{}: + data, err := json.Marshal(c) + if err != nil { + return nil, err + } + err = json.Unmarshal(data, result) + if err != nil { + return nil, err + } + } + + return result, nil +} diff --git a/module/application-authorization/auth-driver/basic/basic.go b/module/application-authorization/auth-driver/basic/basic.go new file mode 100644 index 00000000..b6f1844c --- /dev/null +++ b/module/application-authorization/auth-driver/basic/basic.go @@ -0,0 +1,48 @@ +package basic + +import ( + "encoding/json" + "fmt" + + auth_driver "github.com/APIParkLab/APIPark/module/application-authorization/auth-driver" + + application_authorization_dto "github.com/APIParkLab/APIPark/module/application-authorization/dto" +) + +var _ auth_driver.IAuthConfig = &Config{} + +var ( + driver = "basic" +) + +func init() { + auth_driver.RegisterAuthFactory(driver, auth_driver.NewFactory[Config](driver)) +} + +type Config struct { + UserName string `json:"user_name"` + Password string `json:"password"` + Label map[string]string `json:"label"` +} + +func (cfg *Config) ID() string { + return cfg.UserName +} + +func (cfg *Config) Valid() ([]byte, error) { + if cfg.UserName == "" { + return nil, fmt.Errorf("username is empty") + } + if cfg.Password == "" { + return nil, fmt.Errorf("password is empty") + } + return json.Marshal(cfg) +} + +func (cfg *Config) Detail() []application_authorization_dto.DetailItem { + + return []application_authorization_dto.DetailItem{ + {Key: "用户名", Value: cfg.UserName}, + {Key: "密码", Value: cfg.Password}, + } +} diff --git a/module/application-authorization/auth-driver/jwt/jwt.go b/module/application-authorization/auth-driver/jwt/jwt.go new file mode 100644 index 00000000..b67a54ef --- /dev/null +++ b/module/application-authorization/auth-driver/jwt/jwt.go @@ -0,0 +1,119 @@ +package jwt + +import ( + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + + auth_driver "github.com/APIParkLab/APIPark/module/application-authorization/auth-driver" + + "github.com/eolinker/go-common/utils" + + application_authorization_dto "github.com/APIParkLab/APIPark/module/application-authorization/dto" +) + +const ( + driver = "jwt" +) + +func init() { + auth_driver.RegisterAuthFactory(driver, auth_driver.NewFactory[Config](driver)) +} + +type Config struct { + Iss string `json:"iss"` + Algorithm string `json:"algorithm"` + Secret string `json:"secret"` + PublicKey string `json:"public_key"` + User string `json:"user"` + UserPath string `json:"user_path"` + ClaimsToVerify []string `json:"claims_to_verify"` + Label map[string]string `json:"label"` + SignatureIsBase64 bool `json:"signature_is_base64"` +} + +func (cfg *Config) ID() string { + builder := strings.Builder{} + switch cfg.Algorithm { + case "HS256", "HS384", "HS512": + builder.WriteString(strings.TrimSpace(cfg.Iss)) + builder.WriteString(strings.TrimSpace(cfg.Secret)) + builder.WriteString(strings.TrimSpace(cfg.Algorithm)) + builder.WriteString(strconv.FormatBool(cfg.SignatureIsBase64)) + builder.WriteString(strings.TrimSpace(cfg.UserPath)) + for _, claim := range cfg.ClaimsToVerify { + builder.WriteString(strings.TrimSpace(claim)) + } + + case "RS256", "RS384", "RS512", "ES256", "ES384", "ES512": + builder.WriteString(strings.TrimSpace(cfg.Iss)) + builder.WriteString(strings.TrimSpace(cfg.PublicKey)) + builder.WriteString(strings.TrimSpace(cfg.Algorithm)) + builder.WriteString(strings.TrimSpace(cfg.UserPath)) + for _, claim := range cfg.ClaimsToVerify { + builder.WriteString(strings.TrimSpace(claim)) + } + default: + return "" + } + return utils.Md5(builder.String()) +} + +func (cfg *Config) Valid() ([]byte, error) { + if cfg.Iss == "" { + return nil, errors.New("iss is null") + } + if cfg.Algorithm == "" { + return nil, errors.New("algorithm is null") + } + algorithm := strings.ToUpper(cfg.Algorithm) + switch algorithm { + case "HS256", "HS384", "HS512": + if cfg.Secret == "" { + return nil, errors.New("secret is null") + } + case "RS256", "RS384", "RS512", "ES256", "ES384", "ES512": + if cfg.PublicKey == "" { + return nil, errors.New("public_key is null") + } + default: + return nil, fmt.Errorf("unsupport algorithm") + } + + //校验 校验字段 + for _, claim := range cfg.ClaimsToVerify { + switch claim { + case "exp", "nbf": + default: + return nil, fmt.Errorf("claim key %s is illegal. ", claim) + } + } + return json.Marshal(cfg) +} + +func (cfg *Config) Detail() []application_authorization_dto.DetailItem { + + items := []application_authorization_dto.DetailItem{ + {Key: "Iss", Value: cfg.Iss}, + {Key: "签名算法", Value: cfg.Algorithm}, + {Key: "用户名", Value: cfg.User}, + {Key: "用户名JsonPath", Value: cfg.UserPath}, + {Key: "校验字段", Value: strings.Join(cfg.ClaimsToVerify, ",")}, + } + + switch cfg.Algorithm { + case "HS256", "HS384", "HS512": + items = append(items, application_authorization_dto.DetailItem{Key: "Secret", Value: cfg.Secret}) + base64 := "false" + if cfg.SignatureIsBase64 { + base64 = "true" + } + items = append(items, application_authorization_dto.DetailItem{Key: "Secret", Value: base64}) + default: + items = append(items, application_authorization_dto.DetailItem{Key: "RSA公钥", Value: cfg.PublicKey}) + } + + return items +} diff --git a/module/application-authorization/auth-driver/manager.go b/module/application-authorization/auth-driver/manager.go new file mode 100644 index 00000000..140bf7de --- /dev/null +++ b/module/application-authorization/auth-driver/manager.go @@ -0,0 +1,33 @@ +package auth_driver + +import "github.com/eolinker/eosc" + +var ( + defaultManager = NewManager() +) + +type Manager struct { + authFactory eosc.Untyped[string, IFactory] +} + +func NewManager() *Manager { + return &Manager{ + authFactory: eosc.BuildUntyped[string, IFactory](), + } +} + +func (m *Manager) RegisterAuth(name string, auth IFactory) { + m.authFactory.Set(name, auth) +} + +func (m *Manager) GetAuth(name string) (IFactory, bool) { + return m.authFactory.Get(name) +} + +func GetAuthFactory(name string) (IFactory, bool) { + return defaultManager.GetAuth(name) +} + +func RegisterAuthFactory(name string, auth IFactory) { + defaultManager.RegisterAuth(name, auth) +} diff --git a/module/application-authorization/auth-driver/oauth2/hash.go b/module/application-authorization/auth-driver/oauth2/hash.go new file mode 100644 index 00000000..1f80b00c --- /dev/null +++ b/module/application-authorization/auth-driver/oauth2/hash.go @@ -0,0 +1,45 @@ +package oauth2 + +import ( + "crypto/rand" + "crypto/sha512" + "encoding/base64" + "fmt" + + "golang.org/x/crypto/pbkdf2" +) + +func hashSecret(secret []byte, saltLen int, iterations int, keyLength int) (string, error) { + if saltLen < 1 { + saltLen = 16 + } + salt, err := generateRandomSalt(saltLen) + if err != nil { + return "", err + } + // 迭代次数和密钥长度 + if iterations < 1 { + iterations = 10000 + } + if keyLength < 1 { + keyLength = 32 + } + + // 使用 PBKDF2 密钥派生函数 + key := pbkdf2.Key(secret, salt, iterations, keyLength, sha512.New) + return fmt.Sprintf("$pbkdf2-sha512$i=%d,l=%d$%s$%s", iterations, keyLength, base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(key)), nil +} + +func generateRandomSalt(length int) ([]byte, error) { + // Create a byte slice with the specified length + salt := make([]byte, length) + + // Use crypto/rand to fill the slice with random bytes + _, err := rand.Read(salt) + if err != nil { + return nil, err + } + + // Return the salt as a hexadecimal string + return salt, nil +} diff --git a/module/application-authorization/auth-driver/oauth2/oauth2.go b/module/application-authorization/auth-driver/oauth2/oauth2.go new file mode 100644 index 00000000..1cfd3d8d --- /dev/null +++ b/module/application-authorization/auth-driver/oauth2/oauth2.go @@ -0,0 +1,61 @@ +package oauth2 + +import ( + "encoding/json" + "strconv" + + auth_driver "github.com/APIParkLab/APIPark/module/application-authorization/auth-driver" + + application_authorization_dto "github.com/APIParkLab/APIPark/module/application-authorization/dto" +) + +const ( + driver = "oauth2" +) + +var _ auth_driver.IAuthConfig = (*Config)(nil) + +func init() { + auth_driver.RegisterAuthFactory(driver, auth_driver.NewFactory[Config](driver)) +} + +type Config struct { + ClientId string `json:"client_id" label:"客户端ID"` + ClientSecret string `json:"client_secret" label:"客户端密钥"` + ClientType string `json:"client_type" label:"客户端类型" enum:"public,confidential"` + HashSecret bool `json:"hash_secret" label:"是否Hash加密"` + RedirectUrls []string `json:"redirect_urls" label:"重定向URL列表"` + Hashed bool `json:"hashed"` +} + +func (cfg *Config) ID() string { + return cfg.ClientId +} + +func (cfg *Config) Valid() ([]byte, error) { + + if cfg.HashSecret && !cfg.Hashed { + // 未加密 + secret, err := hashSecret([]byte(cfg.ClientSecret), 0, 0, 0) + if err != nil { + return nil, err + } else { + cfg.ClientSecret = secret + cfg.Hashed = true + } + } + return json.Marshal(cfg) +} + +func (cfg *Config) Detail() []application_authorization_dto.DetailItem { + + redirectURLs, _ := json.Marshal(cfg.RedirectUrls) + + return []application_authorization_dto.DetailItem{ + {Key: "客户端ID", Value: cfg.ClientId}, + {Key: "客户端密钥", Value: cfg.ClientSecret}, + {Key: "客户端类型", Value: cfg.ClientType}, + {Key: "对密钥进行Hash", Value: strconv.FormatBool(cfg.HashSecret)}, + {Key: "重定向URL列表", Value: string(redirectURLs)}, + } +} diff --git a/module/application-authorization/authorization.go b/module/application-authorization/authorization.go new file mode 100644 index 00000000..847528b8 --- /dev/null +++ b/module/application-authorization/authorization.go @@ -0,0 +1,41 @@ +package application_authorization + +import ( + "context" + "reflect" + + application_authorization_dto "github.com/APIParkLab/APIPark/module/application-authorization/dto" + + "github.com/APIParkLab/APIPark/gateway" + + "github.com/eolinker/go-common/autowire" + + _ "github.com/APIParkLab/APIPark/module/application-authorization/auth-driver/aksk" + _ "github.com/APIParkLab/APIPark/module/application-authorization/auth-driver/apikey" + _ "github.com/APIParkLab/APIPark/module/application-authorization/auth-driver/basic" + _ "github.com/APIParkLab/APIPark/module/application-authorization/auth-driver/jwt" + _ "github.com/APIParkLab/APIPark/module/application-authorization/auth-driver/oauth2" +) + +type IAuthorizationModule interface { + // AddAuthorization 添加项目鉴权信息 + AddAuthorization(ctx context.Context, appId string, info *application_authorization_dto.CreateAuthorization) (*application_authorization_dto.Authorization, error) + // EditAuthorization 修改项目鉴权信息 + EditAuthorization(ctx context.Context, appId string, aid string, info *application_authorization_dto.EditAuthorization) (*application_authorization_dto.Authorization, error) + // DeleteAuthorization 删除项目鉴权 + DeleteAuthorization(ctx context.Context, appId string, aid string) error + // Authorizations 获取项目鉴权列表 + Authorizations(ctx context.Context, appId string) ([]*application_authorization_dto.AuthorizationItem, error) + // Detail 获取项目鉴权详情(弹窗用) + Detail(ctx context.Context, appId string, aid string) ([]application_authorization_dto.DetailItem, error) + // Info 获取项目鉴权详情 + Info(ctx context.Context, appId string, aid string) (*application_authorization_dto.Authorization, error) +} + +func init() { + autowire.Auto[IAuthorizationModule](func() reflect.Value { + m := new(imlAuthorizationModule) + gateway.RegisterInitHandleFunc(m.initGateway) + return reflect.ValueOf(m) + }) +} diff --git a/module/application-authorization/dto/input.go b/module/application-authorization/dto/input.go new file mode 100644 index 00000000..a3253359 --- /dev/null +++ b/module/application-authorization/dto/input.go @@ -0,0 +1,21 @@ +package application_authorization_dto + +type CreateAuthorization struct { + UUID string `json:"uuid"` + Name string `json:"name"` + Driver string `json:"driver"` + Position string `json:"position"` + TokenName string `json:"token_name"` + ExpireTime int64 `json:"expire_time"` + Config map[string]interface{} `json:"config"` + HideCredential bool `json:"hide_credential"` +} + +type EditAuthorization struct { + Name *string `json:"name"` + Position *string `json:"position"` + TokenName *string `json:"token_name"` + ExpireTime *int64 `json:"expire_time"` + Config *map[string]interface{} `json:"config"` + HideCredential *bool `json:"hide_credential"` +} diff --git a/module/application-authorization/dto/output.go b/module/application-authorization/dto/output.go new file mode 100644 index 00000000..505b31bd --- /dev/null +++ b/module/application-authorization/dto/output.go @@ -0,0 +1,33 @@ +package application_authorization_dto + +import "github.com/eolinker/go-common/auto" + +type Authorization struct { + UUID string `json:"id"` + Name string `json:"name"` + Driver string `json:"driver"` + Position string `json:"position"` + TokenName string `json:"token_name"` + Config map[string]interface{} `json:"config"` + ExpireTime int64 `json:"expire_time"` + HideCredential bool `json:"hide_credential"` +} + +type AuthorizationItem struct { + Id string `json:"id"` + Name string `json:"name"` + Driver string `json:"driver"` + ExpireTime int64 `json:"expire_time"` + Position string `json:"position"` + TokenName string `json:"token_name"` + Creator auto.Label `json:"creator" aolabel:"user"` + Updater auto.Label `json:"updater" aolabel:"user"` + CreateTime auto.TimeLabel `json:"create_time"` + UpdateTime auto.TimeLabel `json:"update_time"` + HideCredential bool `json:"hide_credential"` +} + +type DetailItem struct { + Key string `json:"key"` + Value string `json:"value"` +} diff --git a/module/application-authorization/iml.go b/module/application-authorization/iml.go new file mode 100644 index 00000000..e8b25e44 --- /dev/null +++ b/module/application-authorization/iml.go @@ -0,0 +1,380 @@ +package application_authorization + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + application_authorization "github.com/APIParkLab/APIPark/service/application-authorization" + + "github.com/eolinker/eosc/log" + + authDriver "github.com/APIParkLab/APIPark/module/application-authorization/auth-driver" + + "github.com/eolinker/go-common/utils" + + "github.com/APIParkLab/APIPark/gateway" + + "github.com/APIParkLab/APIPark/service/cluster" + "github.com/APIParkLab/APIPark/service/service" + + "github.com/eolinker/go-common/auto" + + "github.com/google/uuid" + + "github.com/eolinker/go-common/store" + + application_authorization_dto "github.com/APIParkLab/APIPark/module/application-authorization/dto" +) + +var _ IAuthorizationModule = (*imlAuthorizationModule)(nil) + +type imlAuthorizationModule struct { + serviceService service.IServiceService `autowired:""` + authorizationService application_authorization.IAuthorizationService `autowired:""` + clusterService cluster.IClusterService `autowired:""` + transaction store.ITransaction `autowired:""` +} + +func (i *imlAuthorizationModule) getApplications(ctx context.Context, appIds []string, appMap map[string]*service.Service) ([]*gateway.ApplicationRelease, error) { + authorizations, err := i.authorizationService.ListByApp(ctx, appIds...) + if err != nil { + return nil, err + } + authMap := utils.SliceToMapArray(authorizations, func(a *application_authorization.Authorization) string { + return a.Application + }) + return utils.SliceToSlice(appIds, func(id string) *gateway.ApplicationRelease { + auths := authMap[id] + description := "" + info, ok := appMap[id] + if ok { + description = info.Description + } + return &gateway.ApplicationRelease{ + BasicItem: &gateway.BasicItem{ + ID: id, + Description: description, + Version: time.Now().Format("20060102150405"), + MatchLabels: map[string]string{ + "service": id, + }, + }, + + Authorizations: utils.SliceToSlice(auths, func(a *application_authorization.Authorization) *gateway.Authorization { + authCfg := make(map[string]interface{}) + _ = json.Unmarshal([]byte(a.Config), &authCfg) + return &gateway.Authorization{ + Type: a.Type, + Position: a.Position, + TokenName: a.TokenName, + Expire: a.ExpireTime, + Config: authCfg, + HideCredential: a.HideCredential, + } + }), + } + }), nil +} + +func (i *imlAuthorizationModule) initGateway(ctx context.Context, partitionId string, clientDriver gateway.IClientDriver) error { + services, err := i.serviceService.List(ctx) + if err != nil { + return err + } + serviceIds := make([]string, 0, len(services)) + serviceMap := make(map[string]*service.Service) + for _, p := range services { + serviceIds = append(serviceIds, p.Id) + serviceMap[p.Id] = p + } + + applications, err := i.getApplications(ctx, serviceIds, serviceMap) + if err != nil { + return err + } + return clientDriver.Application().Online(ctx, applications...) +} + +func (i *imlAuthorizationModule) online(ctx context.Context, s *service.Service) error { + + clusters, err := i.clusterService.List(ctx) + if err != nil { + return err + } + authorizations, err := i.authorizationService.ListByApp(ctx, s.Id) + if err != nil { + return err + } + app := &gateway.ApplicationRelease{ + BasicItem: &gateway.BasicItem{ + ID: s.Id, + Description: s.Description, + Version: time.Now().Format("20060102150405"), + MatchLabels: map[string]string{ + "service": s.Id, + }, + }, + Authorizations: utils.SliceToSlice(authorizations, func(a *application_authorization.Authorization) *gateway.Authorization { + authCfg := make(map[string]interface{}) + _ = json.Unmarshal([]byte(a.Config), &authCfg) + return &gateway.Authorization{ + Type: a.Type, + Position: a.Position, + TokenName: a.TokenName, + Expire: a.ExpireTime, + Config: authCfg, + HideCredential: a.HideCredential, + } + }), + } + + for _, c := range clusters { + err := i.doOnline(ctx, c.Uuid, app) + if err != nil { + log.Warnf("service authorization online for cluster[%s] %v", c.Name, err) + } + } + return nil +} +func (i *imlAuthorizationModule) doOnline(ctx context.Context, clusterId string, app *gateway.ApplicationRelease) error { + client, err := i.clusterService.GatewayClient(ctx, clusterId) + if err != nil { + return err + } + defer func() { + _ = client.Close(ctx) + }() + return client.Application().Online(ctx, app) + +} +func (i *imlAuthorizationModule) AddAuthorization(ctx context.Context, appId string, info *application_authorization_dto.CreateAuthorization) (*application_authorization_dto.Authorization, error) { + authFactory, has := authDriver.GetAuthFactory(info.Driver) + if !has { + return nil, fmt.Errorf("unknown driver %s", info.Driver) + } + auth, err := authFactory.Create(info.Config) + if err != nil { + return nil, err + } + cfg, err := auth.AuthConfig().Valid() + if err != nil { + return nil, err + } + + s, err := i.serviceService.Get(ctx, appId) + if err != nil { + return nil, err + } + + if info.UUID == "" { + info.UUID = uuid.New().String() + } + + // 缺少配置查重操作 + err = i.transaction.Transaction(ctx, func(ctx context.Context) error { + err = i.authorizationService.Create(ctx, &application_authorization.Create{ + UUID: info.UUID, + Application: appId, + Name: info.Name, + Type: info.Driver, + Position: info.Position, + TokenName: info.TokenName, + Config: string(cfg), + ExpireTime: info.ExpireTime, + HideCredential: info.HideCredential, + AuthID: auth.GenerateID(info.Position, info.TokenName), + }) + if err != nil { + return err + } + + return i.online(ctx, s) + }) + if err != nil { + return nil, err + } + + return i.Info(ctx, appId, info.UUID) +} + +func (i *imlAuthorizationModule) EditAuthorization(ctx context.Context, appId string, aid string, info *application_authorization_dto.EditAuthorization) (*application_authorization_dto.Authorization, error) { + authInfo, err := i.authorizationService.Get(ctx, aid) + if err != nil { + return nil, err + } + authFactory, has := authDriver.GetAuthFactory(authInfo.Type) + if !has { + return nil, fmt.Errorf("unknown driver %s", authInfo.Type) + } + auth, err := authFactory.Create(info.Config) + if err != nil { + return nil, err + } + cfg, err := auth.AuthConfig().Valid() + if err != nil { + return nil, err + } + + appInfo, err := i.serviceService.Get(ctx, appId) + if err != nil { + return nil, err + } + + err = i.transaction.Transaction(ctx, func(ctx context.Context) error { + authId := auth.GenerateID(authInfo.Position, authInfo.TokenName) + cfgStr := string(cfg) + err = i.authorizationService.Save(ctx, aid, &application_authorization.Edit{ + Name: info.Name, + Position: info.Position, + TokenName: info.TokenName, + ExpireTime: info.ExpireTime, + HideCredential: info.HideCredential, + AuthID: &authId, + Config: &cfgStr, + }) + if err != nil { + return err + } + return i.online(ctx, appInfo) + }) + + if err != nil { + return nil, err + } + return i.Info(ctx, appId, aid) +} + +func (i *imlAuthorizationModule) DeleteAuthorization(ctx context.Context, pid string, aid string) error { + _, err := i.serviceService.Get(ctx, pid) + if err != nil { + return err + } + + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + err = i.authorizationService.Delete(ctx, aid) + if err != nil { + return err + } + clusters, err := i.clusterService.List(ctx) + if err != nil { + return err + } + app := &gateway.ApplicationRelease{ + BasicItem: &gateway.BasicItem{ + ID: pid, + }, + } + for _, c := range clusters { + err := i.doOffline(ctx, c.Uuid, app) + if err != nil { + log.Warnf("service authorization offline for cluster[%s] %v", c.Name, err) + } + } + return nil + }) +} +func (i *imlAuthorizationModule) doOffline(ctx context.Context, clusterId string, app *gateway.ApplicationRelease) error { + client, err := i.clusterService.GatewayClient(ctx, clusterId) + if err != nil { + return err + } + defer func() { + _ = client.Close(ctx) + }() + return client.Application().Offline(ctx, app) + +} +func (i *imlAuthorizationModule) Authorizations(ctx context.Context, pid string) ([]*application_authorization_dto.AuthorizationItem, error) { + _, err := i.serviceService.Get(ctx, pid) + if err != nil { + return nil, err + } + authorizations, err := i.authorizationService.ListByApp(ctx, pid) + if err != nil { + return nil, err + } + result := make([]*application_authorization_dto.AuthorizationItem, 0, len(authorizations)) + for _, a := range authorizations { + result = append(result, &application_authorization_dto.AuthorizationItem{ + Id: a.UUID, + Name: a.Name, + Driver: a.Type, + ExpireTime: a.ExpireTime, + Position: a.Position, + TokenName: a.TokenName, + Creator: auto.UUID(a.Creator), + Updater: auto.UUID(a.Updater), + CreateTime: auto.TimeLabel(a.CreateTime), + UpdateTime: auto.TimeLabel(a.UpdateTime), + HideCredential: a.HideCredential, + }) + } + return result, nil +} + +func (i *imlAuthorizationModule) Detail(ctx context.Context, pid string, aid string) ([]application_authorization_dto.DetailItem, error) { + _, err := i.serviceService.Get(ctx, pid) + if err != nil { + return nil, err + } + authInfo, err := i.authorizationService.Get(ctx, aid) + if err != nil { + return nil, err + } + authFactory, has := authDriver.GetAuthFactory(authInfo.Type) + if !has { + return nil, errors.New("unknown driver") + } + auth, err := authFactory.Create(authInfo.Config) + if err != nil { + return nil, err + } + cfgItems := auth.AuthConfig().Detail() + details := make([]application_authorization_dto.DetailItem, 0, 6+len(cfgItems)) + details = append(details, application_authorization_dto.DetailItem{Key: "名称", Value: authInfo.Name}) + details = append(details, application_authorization_dto.DetailItem{Key: "鉴权类型", Value: authInfo.Type}) + details = append(details, application_authorization_dto.DetailItem{Key: "参数位置", Value: authInfo.Position}) + details = append(details, application_authorization_dto.DetailItem{Key: "参数名", Value: authInfo.TokenName}) + details = append(details, cfgItems...) + dateStr := "永久" + if authInfo.ExpireTime != 0 { + dateStr = time.Unix(authInfo.ExpireTime, 0).Format("2006-01-02") + } + details = append(details, application_authorization_dto.DetailItem{Key: "过期日期", Value: dateStr}) + hideAuthStr := "是" + if !authInfo.HideCredential { + hideAuthStr = "否" + } + details = append(details, application_authorization_dto.DetailItem{Key: "隐藏鉴权信息", Value: hideAuthStr}) + + return details, nil +} + +func (i *imlAuthorizationModule) Info(ctx context.Context, pid string, aid string) (*application_authorization_dto.Authorization, error) { + _, err := i.serviceService.Get(ctx, pid) + if err != nil { + return nil, err + } + auth, err := i.authorizationService.Get(ctx, aid) + if err != nil { + return nil, err + } + var cfg map[string]interface{} + if auth.Config != "" { + _ = json.Unmarshal([]byte(auth.Config), &cfg) + } + + return &application_authorization_dto.Authorization{ + UUID: auth.UUID, + Name: auth.Name, + Driver: auth.Type, + Position: auth.Position, + TokenName: auth.TokenName, + ExpireTime: auth.ExpireTime, + HideCredential: auth.HideCredential, + Config: cfg, + }, nil +} diff --git a/module/catalogue/catalogue.go b/module/catalogue/catalogue.go new file mode 100644 index 00000000..7ef4ab5b --- /dev/null +++ b/module/catalogue/catalogue.go @@ -0,0 +1,34 @@ +package catalogue + +import ( + "context" + "reflect" + + "github.com/eolinker/go-common/autowire" + + catalogue_dto "github.com/APIParkLab/APIPark/module/catalogue/dto" +) + +type ICatalogueModule interface { + // Search 搜索目录 + Search(ctx context.Context, keyword string) ([]*catalogue_dto.Item, error) + // Create 创建目录 + Create(ctx context.Context, input *catalogue_dto.CreateCatalogue) error + // Edit 编辑目录 + Edit(ctx context.Context, id string, input *catalogue_dto.EditCatalogue) error + // Delete 删除目录 + Delete(ctx context.Context, id string) error + // Services 关键字筛选服务列表 + Services(ctx context.Context, keyword string) ([]*catalogue_dto.ServiceItem, error) + // ServiceDetail 服务详情 + ServiceDetail(ctx context.Context, sid string) (*catalogue_dto.ServiceDetail, error) + // Subscribe 订阅服务 + Subscribe(ctx context.Context, subscribeInfo *catalogue_dto.SubscribeService) error + Sort(ctx context.Context, sorts []*catalogue_dto.SortItem) error +} + +func init() { + autowire.Auto[ICatalogueModule](func() reflect.Value { + return reflect.ValueOf(new(imlCatalogueModule)) + }) +} diff --git a/module/catalogue/dto/input.go b/module/catalogue/dto/input.go new file mode 100644 index 00000000..11fb91b7 --- /dev/null +++ b/module/catalogue/dto/input.go @@ -0,0 +1,23 @@ +package catalogue_dto + +type CreateCatalogue struct { + Id string `json:"id"` + Name string `json:"name"` + Parent *string `json:"parent" aocheck:"catalogue"` +} + +type EditCatalogue struct { + Name *string `json:"name"` + Parent *string `json:"parent" aocheck:"catalogue"` +} + +type SubscribeService struct { + Service string `json:"service"` + Applications []string `json:"applications" aocheck:"project"` + Reason string `json:"reason"` +} + +type SortItem struct { + Id string `json:"id"` + Children []*SortItem `json:"children"` +} diff --git a/module/catalogue/dto/output.go b/module/catalogue/dto/output.go new file mode 100644 index 00000000..309aa068 --- /dev/null +++ b/module/catalogue/dto/output.go @@ -0,0 +1,62 @@ +package catalogue_dto + +import "github.com/eolinker/go-common/auto" + +type Item struct { + Id string `json:"id"` + Name string `json:"name"` + Children []*Item `json:"children"` +} + +type ServiceItem struct { + Id string `json:"id"` + Name string `json:"name"` + Tags []auto.Label `json:"tags" aolabel:"tag"` + Catalogue auto.Label `json:"catalogue" aolabel:"catalogue"` + Description string `json:"description"` + Logo string `json:"logo"` + ApiNum int64 `json:"api_num"` + SubscriberNum int64 `json:"subscriber_num"` +} + +type ServiceDetail struct { + Name string `json:"name"` + Description string `json:"description"` + Document string `json:"document"` + Basic *ServiceBasic `json:"basic"` + Apis []*ServiceApi `json:"apis"` +} + +type ServiceBasic struct { + Team auto.Label `json:"team" aolabel:"team"` + ApiNum int `json:"api_num"` + AppNum int `json:"app_num"` + Tags []auto.Label `json:"tags" aolabel:"tag"` + Catalogue auto.Label `json:"catalogue" aolabel:"catalogue"` + Version string `json:"version"` + UpdateTime auto.TimeLabel `json:"update_time"` + Logo string `json:"logo"` +} + +type ServiceApiBasic struct { + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Method string `json:"method"` + Path string `json:"path"` + Creator auto.Label `json:"creator" aolabel:"user"` + Updater auto.Label `json:"updater" aolabel:"user"` + CreateTime auto.TimeLabel `json:"create_time"` + UpdateTime auto.TimeLabel `json:"update_time"` +} + +type ServiceApi struct { + *ServiceApiBasic + Doc interface{} `json:"doc"` +} + +type Partition struct { + Id string `json:"id"` + Name string `json:"name"` + Prefix string `json:"prefix"` +} diff --git a/module/catalogue/iml.go b/module/catalogue/iml.go new file mode 100644 index 00000000..9e6be241 --- /dev/null +++ b/module/catalogue/iml.go @@ -0,0 +1,495 @@ +package catalogue + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "math" + "sort" + + service_doc "github.com/APIParkLab/APIPark/service/service-doc" + + service_tag "github.com/APIParkLab/APIPark/service/service-tag" + + "github.com/APIParkLab/APIPark/service/subscribe" + + "github.com/eolinker/go-common/store" + + "gorm.io/gorm" + + "github.com/eolinker/go-common/utils" + + "github.com/APIParkLab/APIPark/service/release" + + "github.com/APIParkLab/APIPark/service/api" + "github.com/eolinker/go-common/auto" + + "github.com/APIParkLab/APIPark/service/tag" + + "github.com/APIParkLab/APIPark/service/service" + + "github.com/google/uuid" + + "github.com/APIParkLab/APIPark/service/catalogue" + + catalogue_dto "github.com/APIParkLab/APIPark/module/catalogue/dto" +) + +var ( + _ ICatalogueModule = (*imlCatalogueModule)(nil) + _sortMax = math.MaxInt32 / 2 +) + +type imlCatalogueModule struct { + catalogueService catalogue.ICatalogueService `autowired:""` + apiService api.IAPIService `autowired:""` + serviceService service.IServiceService `autowired:""` + serviceTagService service_tag.ITagService `autowired:""` + serviceDocService service_doc.IDocService `autowired:""` + tagService tag.ITagService `autowired:""` + releaseService release.IReleaseService `autowired:""` + subscribeService subscribe.ISubscribeService `autowired:""` + subscribeApplyService subscribe.ISubscribeApplyService `autowired:""` + transaction store.ITransaction `autowired:""` + + root *Root +} + +func (i *imlCatalogueModule) Subscribe(ctx context.Context, subscribeInfo *catalogue_dto.SubscribeService) error { + if len(subscribeInfo.Applications) == 0 { + return fmt.Errorf("applications is empty") + } + // 获取服务的基本信息 + s, err := i.serviceService.Get(ctx, subscribeInfo.Service) + if err != nil { + return fmt.Errorf("get service failed: %w", err) + } + if !s.AsServer { + return fmt.Errorf("service does not support subscribe") + } + + userId := utils.UserId(ctx) + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + + apps := make([]string, 0, len(subscribeInfo.Applications)) + + for _, appId := range subscribeInfo.Applications { + if appId == s.Id { + // 不能订阅自己 + continue + } + + appInfo, err := i.serviceService.Get(ctx, appId) + if err != nil { + return err + } + if !appInfo.AsApp { + // 当系统不可作为订阅方时,不可订阅 + continue + } + //info, err := i.subscribeApplyService.GetApply(ctx, subscribeInfo.Service, appId) + //if err != nil { + // if !errors.Is(err, gorm.ErrRecordNotFound) { + // return err + // } + err = i.subscribeApplyService.Create(ctx, &subscribe.CreateApply{ + Uuid: uuid.New().String(), + Service: subscribeInfo.Service, + Team: s.Team, + Application: appId, + ApplyTeam: appInfo.Team, + Reason: subscribeInfo.Reason, + Status: subscribe.ApplyStatusReview, + Applier: userId, + }) + + //} else { + // status := subscribe.ApplyStatusReview + // err = i.subscribeApplyService.Save(ctx, info.Id, &subscribe.EditApply{ + // Status: &status, + // Applier: &userId, + // }) + //} + if err != nil { + return err + } + + // 修改订阅表状态 + subscribers, err := i.subscribeService.ListByApplication(ctx, subscribeInfo.Service, appId) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + err = i.subscribeService.Create(ctx, &subscribe.CreateSubscribe{ + Uuid: uuid.New().String(), + Service: subscribeInfo.Service, + Application: appId, + ApplyStatus: subscribe.ApplyStatusReview, + From: subscribe.FromSubscribe, + }) + if err != nil { + return err + } + + } else { + subscriberMap := utils.SliceToMap(subscribers, func(t *subscribe.Subscribe) string { + return t.Application + }) + v, has := subscriberMap[appId] + if !has { + err = i.subscribeService.Create(ctx, &subscribe.CreateSubscribe{ + Uuid: uuid.New().String(), + Service: subscribeInfo.Service, + Application: appId, + ApplyStatus: subscribe.ApplyStatusReview, + From: subscribe.FromSubscribe, + }) + if err != nil { + return err + } + } else if v.ApplyStatus != subscribe.ApplyStatusSubscribe { + status := subscribe.ApplyStatusReview + err = i.subscribeService.Save(ctx, v.Id, &subscribe.UpdateSubscribe{ + ApplyStatus: &status, + }) + } + + } + + apps = append(apps, appId) + } + if len(apps) == 0 { + return fmt.Errorf("no available apps") + } + return nil + }) + +} + +func (i *imlCatalogueModule) ServiceDetail(ctx context.Context, sid string) (*catalogue_dto.ServiceDetail, error) { + // 获取服务的基本信息 + s, err := i.serviceService.Get(ctx, sid) + if err != nil { + return nil, fmt.Errorf("get service failed: %w", err) + } + docStr := "" + doc, err := i.serviceDocService.Get(ctx, sid) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("get service doc failed: %w", err) + } + } else { + docStr = doc.Doc + } + + r, err := i.releaseService.GetRunning(ctx, s.Id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return &catalogue_dto.ServiceDetail{ + Name: s.Name, + Description: s.Description, + Document: docStr, + Basic: &catalogue_dto.ServiceBasic{ + Team: auto.UUID(s.Team), + ApiNum: 0, + }, + }, nil + } + + return nil, fmt.Errorf("get running release failed: %w", err) + } + _, docCommits, _, err := i.releaseService.GetReleaseInfos(ctx, r.UUID) + if err != nil { + return nil, fmt.Errorf("get release apis failed: %w", err) + } + apiMap := make(map[string]*release.APIDocumentCommit) + apiIds := make([]string, 0, len(docCommits)) + for _, v := range docCommits { + apiIds = append(apiIds, v.API) + apiMap[v.API] = v + } + apiList, err := i.apiService.ListInfo(ctx, apiIds...) + if err != nil { + return nil, err + } + + apis := make([]*catalogue_dto.ServiceApi, 0, len(apiList)) + for _, info := range apiList { + basicApi := &catalogue_dto.ServiceApiBasic{ + Id: info.UUID, + Name: info.Name, + Description: info.Description, + Method: info.Method, + Path: info.Path, + Creator: auto.UUID(info.Creator), + Updater: auto.UUID(info.Updater), + CreateTime: auto.TimeLabel(info.CreateAt), + UpdateTime: auto.TimeLabel(info.UpdateAt), + } + v, ok := apiMap[info.UUID] + if !ok { + continue + } + commit, err := i.apiService.GetDocumentCommit(ctx, v.Commit) + if err != nil { + return nil, err + } + tmp := make(map[string]interface{}) + if commit.Data != nil { + err = json.Unmarshal([]byte(commit.Data.Content), &tmp) + if err != nil { + return nil, err + } + } + + apis = append(apis, &catalogue_dto.ServiceApi{ + ServiceApiBasic: basicApi, + Doc: tmp, + }) + } + countMap, err := i.subscribeService.CountMapByService(ctx, subscribe.ApplyStatusSubscribe, sid) + if err != nil { + return nil, err + } + tags, err := i.serviceTagService.List(ctx, []string{sid}, nil) + if err != nil { + return nil, err + } + tagIds := utils.SliceToSlice(tags, func(t *service_tag.Tag) string { + return t.Tid + }, func(t *service_tag.Tag) bool { + return t.Sid == sid + }) + return &catalogue_dto.ServiceDetail{ + Name: s.Name, + Description: s.Description, + Document: docStr, + Basic: &catalogue_dto.ServiceBasic{ + Team: auto.UUID(s.Team), + ApiNum: len(apis), + AppNum: int(countMap[s.Id]), + Tags: auto.List(tagIds), + Catalogue: auto.UUID(s.Catalogue), + Version: r.Version, + UpdateTime: auto.TimeLabel(r.CreateAt), + Logo: s.Logo, + }, + Apis: apis, + }, nil +} + +func (i *imlCatalogueModule) Services(ctx context.Context, keyword string) ([]*catalogue_dto.ServiceItem, error) { + + serviceTags, err := i.serviceTagService.List(ctx, nil, nil) + if err != nil { + return nil, err + } + serviceTagMap := utils.SliceToMapArrayO(serviceTags, func(t *service_tag.Tag) (string, string) { + return t.Sid, t.Tid + }) + + items, err := i.serviceService.SearchPublicServices(ctx, keyword) + if err != nil { + return nil, err + } + serviceIds := utils.SliceToSlice(items, func(i *service.Service) string { + return i.Id + }, func(s *service.Service) bool { + // 未发布的不给展示 + _, err = i.releaseService.GetRunning(ctx, s.Id) + return err == nil + }) + if len(serviceIds) < 1 { + return nil, nil + } + + // 获取服务API数量 + apiCountMap, err := i.apiService.CountMapByService(ctx, serviceIds...) + if err != nil { + return nil, err + } + + subscriberCountMap, err := i.subscribeService.CountMapByService(ctx, subscribe.ApplyStatusSubscribe, serviceIds...) + if err != nil { + return nil, err + } + + result := make([]*catalogue_dto.ServiceItem, 0, len(items)) + for _, v := range items { + apiNum, ok := apiCountMap[v.Id] + if !ok || apiNum < 1 { + continue + } + + result = append(result, &catalogue_dto.ServiceItem{ + Id: v.Id, + Name: v.Name, + Tags: auto.List(serviceTagMap[v.Id]), + Catalogue: auto.UUID(v.Catalogue), + ApiNum: apiNum, + SubscriberNum: subscriberCountMap[v.Id], + Description: v.Description, + Logo: v.Logo, + }) + } + sort.Slice(result, func(i, j int) bool { + if result[i].SubscriberNum != result[j].SubscriberNum { + return result[i].SubscriberNum > result[j].SubscriberNum + } + if result[i].ApiNum != result[j].ApiNum { + return result[i].ApiNum > result[j].ApiNum + } + return result[i].Name < result[j].Name + }) + return result, nil +} + +func (i *imlCatalogueModule) recurseUpdateSort(ctx context.Context, parent string, sorts []*catalogue_dto.SortItem) error { + for index, item := range sorts { + err := i.catalogueService.Save(ctx, item.Id, &catalogue.EditCatalogue{ + Parent: &parent, + Sort: &index, + }) + if err != nil { + return err + } + if len(item.Children) < 1 { + continue + } + return i.recurseUpdateSort(ctx, item.Id, item.Children) + } + return nil +} + +func (i *imlCatalogueModule) Sort(ctx context.Context, sorts []*catalogue_dto.SortItem) error { + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + err := i.recurseUpdateSort(ctx, "", sorts) + if err != nil { + return err + } + all, err := i.catalogueService.List(ctx) + if err != nil { + return err + } + i.root = NewRoot(all) + return nil + }) + +} + +func (i *imlCatalogueModule) Search(ctx context.Context, keyword string) ([]*catalogue_dto.Item, error) { + all, err := i.catalogueService.List(ctx) + if err != nil { + return nil, err + } + if keyword == "" { + parentMap := make(map[string][]*catalogue.Catalogue) + nodeMap := make(map[string]*catalogue.Catalogue) + for _, v := range all { + if _, ok := parentMap[v.Parent]; !ok { + parentMap[v.Parent] = make([]*catalogue.Catalogue, 0) + } + parentMap[v.Parent] = append(parentMap[v.Parent], v) + nodeMap[v.Id] = v + } + return treeItems("", parentMap), nil + } + + catalogues, err := i.catalogueService.Search(ctx, keyword, nil) + if err != nil { + return nil, err + } + if i.root == nil { + // 初始化 + i.root = NewRoot(all) + } + items := make([]*catalogue_dto.Item, 0, len(catalogues)) + + return items, nil +} + +func (i *imlCatalogueModule) Create(ctx context.Context, input *catalogue_dto.CreateCatalogue) error { + parent := "" + if input.Parent != nil { + parent = *input.Parent + } + if input.Id == "" { + input.Id = uuid.New().String() + } + err := i.catalogueService.Create(ctx, &catalogue.CreateCatalogue{ + Id: input.Id, + Name: input.Name, + Parent: parent, + Sort: _sortMax, + }) + if err != nil { + return err + } + // 重新初始化 + catalogues, err := i.catalogueService.List(ctx) + if err != nil { + return err + } + i.root = NewRoot(catalogues) + return nil +} + +func (i *imlCatalogueModule) Edit(ctx context.Context, id string, input *catalogue_dto.EditCatalogue) error { + err := i.catalogueService.Save(ctx, id, &catalogue.EditCatalogue{ + Name: input.Name, + Parent: input.Parent, + }) + if err != nil { + return err + } + // 重新初始化 + catalogues, err := i.catalogueService.List(ctx) + if err != nil { + return err + } + i.root = NewRoot(catalogues) + return nil +} + +func (i *imlCatalogueModule) Delete(ctx context.Context, id string) error { + if id == "" { + return nil + } + list, err := i.catalogueService.Search(ctx, "", map[string]interface{}{ + "parent": id, + }) + if err != nil { + return err + } + if len(list) > 0 { + return fmt.Errorf("该目录下存在子目录") + } + err = i.catalogueService.Delete(ctx, id) + if err != nil { + return err + } + // 重新初始化 + catalogues, err := i.catalogueService.List(ctx) + if err != nil { + return err + } + i.root = NewRoot(catalogues) + return nil +} + +// treeItems 获取子树 +func treeItems(parentId string, parentMap map[string][]*catalogue.Catalogue) []*catalogue_dto.Item { + items := make([]*catalogue_dto.Item, 0) + if _, ok := parentMap[parentId]; ok { + for _, v := range parentMap[parentId] { + childItems := treeItems(v.Id, parentMap) + items = append(items, &catalogue_dto.Item{ + Id: v.Id, + Name: v.Name, + Children: childItems, + }) + } + } + return items +} diff --git a/module/catalogue/tree.go b/module/catalogue/tree.go new file mode 100644 index 00000000..ac25b6c8 --- /dev/null +++ b/module/catalogue/tree.go @@ -0,0 +1,147 @@ +package catalogue + +import ( + "sort" + + "github.com/APIParkLab/APIPark/service/catalogue" +) + +type Group struct { + Uuid string + Name string + Parent string + Depth int + sort int + children []*Group + parents []string +} +type Root struct { + nodes map[string]*Group + list []*Group +} + +func (r *Root) GetParents(uuid string) []string { + if uuid == "" { + return nil + } + n, has := r.nodes[uuid] + if has { + return n.parents + } + return nil +} + +func (r *Root) GetSub(uuid string) []string { + if uuid == "" { + uuids := make([]string, 0, len(r.list)) + for _, g := range r.list { + uuids = append(uuids, g.Uuid) + } + return uuids + } + n, has := r.nodes[uuid] + if has { + return n.SubId() + } + return nil +} +func NewRoot(list []*catalogue.Catalogue) *Root { + m := make(map[string]*Group) + l := make([]*Group, 0, len(list)) + for _, i := range list { + g := &Group{ + Uuid: i.Id, + Parent: i.Parent, + Name: i.Name, + Depth: 0, + children: nil, + } + l = append(l, g) + m[g.Uuid] = g + } + parentMap := make(map[string][]string) + root := new(Group) + for _, i := range l { + if i.Parent != "" { + p, has := m[i.Parent] + if !has { + i.Parent = "" + root.children = append(root.children, i) + } else { + p.children = append(p.children, i) + p.parents = getParents(i.Parent, m, parentMap) + } + + } else { + root.children = append(root.children, i) + } + } + root.ResetDepth(-1) + sort.Sort(Groups(l)) + return &Root{nodes: m, list: l} +} + +func getParents(parentID string, nodeMap map[string]*Group, parentMap map[string][]string) []string { + if parentID == "" { + return nil + } + if parents, has := parentMap[parentID]; has { + return parents + } + parents := make([]string, 0) + node, has := nodeMap[parentID] + if !has { + return nil + } + parents = append(parents, parentID) + tmp := getParents(node.Parent, nodeMap, parentMap) + if tmp != nil { + parents = append(parents, tmp...) + } + return parents +} + +func (g *Group) ResetDepth(d int) { + g.Depth = d + next := d + 1 + for _, c := range g.children { + c.ResetDepth(next) + } +} +func (g *Group) SubId() []string { + if len(g.children) > 0 { + subs := make([]string, 0, len(g.children)) + for _, c := range g.children { + subs = append(subs, c.Uuid) + sc := c.SubId() + if len(sc) > 0 { + subs = append(subs, sc...) + } + } + return subs + } + return nil +} + +type Groups []*Group + +func (g Groups) Len() int { + return len(g) +} + +func (g Groups) Less(i, j int) bool { + if g[i].Depth == g[j].Depth { + if g[i].Parent == g[i].Parent { + if g[i].sort == g[j].sort { + return g[i].Uuid < g[j].Uuid + } + return g[i].sort < g[j].sort + } + return g[i].Parent < g[i].Parent + } + return g[i].Depth < g[j].Depth +} + +func (g Groups) Swap(i, j int) { + g[i], g[j] = g[j], g[i] +} diff --git a/module/certificate/certificate.go b/module/certificate/certificate.go new file mode 100644 index 00000000..b539f4ad --- /dev/null +++ b/module/certificate/certificate.go @@ -0,0 +1,27 @@ +package certificate + +import ( + "context" + "reflect" + + "github.com/APIParkLab/APIPark/gateway" + + certificate_dto "github.com/APIParkLab/APIPark/module/certificate/dto" + "github.com/eolinker/go-common/autowire" +) + +type ICertificateModule interface { + Create(ctx context.Context, create *certificate_dto.FileInput) error + Update(ctx context.Context, id string, edit *certificate_dto.FileInput) error + List(ctx context.Context) ([]*certificate_dto.Certificate, error) + Detail(ctx context.Context, id string) (*certificate_dto.Certificate, *certificate_dto.File, error) + Delete(ctx context.Context, id string) error +} + +func init() { + autowire.Auto[ICertificateModule](func() reflect.Value { + m := new(imlCertificate) + gateway.RegisterInitHandleFunc(m.initGateway) + return reflect.ValueOf(m) + }) +} diff --git a/module/certificate/dto/Create.go b/module/certificate/dto/Create.go new file mode 100644 index 00000000..4c02563e --- /dev/null +++ b/module/certificate/dto/Create.go @@ -0,0 +1,6 @@ +package certificate_dto + +type FileInput struct { + Key string `json:"key"` + Cert string `json:"pem"` +} diff --git a/module/certificate/dto/dto.go b/module/certificate/dto/dto.go new file mode 100644 index 00000000..e071b46a --- /dev/null +++ b/module/certificate/dto/dto.go @@ -0,0 +1,35 @@ +package certificate_dto + +import ( + "github.com/APIParkLab/APIPark/service/certificate" + "github.com/eolinker/go-common/auto" +) + +type Certificate struct { + Id string `json:"id"` + Name string `json:"name"` + Domains []string `json:"domains"` + Partition string `json:"partition"` + NotBefore auto.TimeLabel `json:"not_before"` + NotAfter auto.TimeLabel `json:"not_after"` + Updater auto.Label `json:"updater" aolabel:"user"` + UpdateTime auto.TimeLabel `json:"update_time,omitempty"` +} + +func FromModel(c *certificate.Certificate) *Certificate { + return &Certificate{ + Id: c.ID, + Name: c.Name, + Domains: c.Domains, + Partition: c.Cluster, + NotBefore: auto.TimeLabel(c.NotBefore), + NotAfter: auto.TimeLabel(c.NotAfter), + Updater: auto.UUID(c.Updater), + UpdateTime: auto.TimeLabel(c.UpdateTime), + } +} + +type File struct { + Key string `json:"key"` + Cert string `json:"pem"` +} diff --git a/module/certificate/impl.go b/module/certificate/impl.go new file mode 100644 index 00000000..2c05eb89 --- /dev/null +++ b/module/certificate/impl.go @@ -0,0 +1,233 @@ +package certificate + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "time" + + "github.com/eolinker/eosc/log" + "gorm.io/gorm" + + "github.com/APIParkLab/APIPark/gateway" + + "github.com/google/uuid" + + "github.com/APIParkLab/APIPark/service/cluster" + "github.com/eolinker/go-common/store" + + certificatedto "github.com/APIParkLab/APIPark/module/certificate/dto" + "github.com/APIParkLab/APIPark/service/certificate" + "github.com/eolinker/ap-account/service/account" + "github.com/eolinker/go-common/utils" +) + +var ( + _ ICertificateModule = (*imlCertificate)(nil) +) + +type imlCertificate struct { + service certificate.ICertificateService `autowired:""` + userInfoService account.IAccountService `autowired:""` + clusterService cluster.IClusterService `autowired:""` + transaction store.ITransaction `autowired:""` +} + +func (m *imlCertificate) getCertificates(ctx context.Context, clusterId string) ([]*gateway.DynamicRelease, error) { + certs, err := m.service.List(ctx, clusterId) + if err != nil { + return nil, err + } + publishCerts := make([]*gateway.DynamicRelease, 0, len(certs)) + for _, cert := range certs { + _, certFile, err := m.service.Get(ctx, cert.ID) + if err != nil { + return nil, err + } + publishCerts = append(publishCerts, &gateway.DynamicRelease{ + BasicItem: &gateway.BasicItem{ + ID: cert.ID, + Description: "", + Version: cert.UpdateTime.Format("20060102150405"), + MatchLabels: map[string]string{ + "module": "certificate", + }, + }, + Attr: map[string]interface{}{ + "key": certFile.Key, + "pem": certFile.Cert, + }, + }) + } + return publishCerts, nil +} + +func (m *imlCertificate) initGateway(ctx context.Context, clusterId string, clientDriver gateway.IClientDriver) error { + certificateClient, err := clientDriver.Dynamic("certificate") + if err != nil { + return err + } + certs, err := m.getCertificates(ctx, clusterId) + if err != nil { + return err + } + return certificateClient.Online(ctx, certs...) +} + +func (m *imlCertificate) save(ctx context.Context, id string, clusterId string, create *certificatedto.FileInput) (*certificatedto.Certificate, error) { + + keyData, err := base64.StdEncoding.DecodeString(create.Key) + if err != nil { + + return nil, fmt.Errorf("decode key error: %w", err) + } + certData, err := base64.StdEncoding.DecodeString(create.Cert) + if err != nil { + return nil, fmt.Errorf("decode cert error: %w", err) + } + o, err := m.service.Save(ctx, id, clusterId, keyData, certData) + if err != nil { + return nil, err + } + out := certificatedto.FromModel(o) + return out, nil +} + +func (m *imlCertificate) syncGateway(ctx context.Context, clusterId string, releaseInfo *gateway.DynamicRelease, online bool) error { + client, err := m.clusterService.GatewayClient(ctx, clusterId) + if err != nil { + return err + } + defer func() { + err := client.Close(ctx) + if err != nil { + log.Warn("close apinto client:", err) + } + }() + dynamicClient, err := client.Dynamic("certificate") + if err != nil { + return err + } + if online { + return dynamicClient.Online(ctx, releaseInfo) + } + return dynamicClient.Offline(ctx, releaseInfo) +} + +func (m *imlCertificate) Create(ctx context.Context, create *certificatedto.FileInput) error { + + return m.transaction.Transaction(ctx, func(ctx context.Context) error { + id := uuid.New().String() + version := time.Now().Format("20060102150405") + err := m.syncGateway(ctx, cluster.DefaultClusterID, &gateway.DynamicRelease{ + BasicItem: &gateway.BasicItem{ + ID: id, + Description: "", + Version: version, + MatchLabels: map[string]string{ + "module": "certificate", + }, + }, + Attr: map[string]interface{}{ + "key": create.Key, + "pem": create.Cert, + }, + }, true) + if err != nil { + return err + } + _, err = m.save(ctx, id, cluster.DefaultClusterID, create) + if err != nil { + return err + } + return nil + }) + +} + +func (m *imlCertificate) Update(ctx context.Context, id string, edit *certificatedto.FileInput) error { + old, _, err := m.service.Get(ctx, id) + if err != nil { + return err + } + clusters, err := m.clusterService.ListByClusters(ctx, old.Cluster) + if err != nil { + return err + } + return m.transaction.Transaction(ctx, func(ctx context.Context) error { + version := time.Now().Format("20060102150405") + for _, c := range clusters { + err = m.syncGateway(ctx, c.Uuid, &gateway.DynamicRelease{ + BasicItem: &gateway.BasicItem{ + ID: id, + Description: "", + Version: version, + MatchLabels: map[string]string{ + "module": "certificate", + }, + }, + Attr: map[string]interface{}{ + "key": edit.Key, + "pem": edit.Cert, + }, + }, true) + if err != nil { + return err + } + } + _, err = m.save(ctx, id, old.Cluster, edit) + if err != nil { + return err + } + return nil + }) +} +func (m *imlCertificate) List(ctx context.Context) ([]*certificatedto.Certificate, error) { + certs, err := m.service.List(ctx, cluster.DefaultClusterID) + if err != nil { + return nil, err + } + outList := utils.SliceToSlice(certs, certificatedto.FromModel) + return outList, nil +} + +func (m *imlCertificate) Detail(ctx context.Context, id string) (*certificatedto.Certificate, *certificatedto.File, error) { + get, f, err := m.service.Get(ctx, id) + if err != nil { + return nil, nil, err + } + out := certificatedto.FromModel(get) + return out, &certificatedto.File{ + Key: base64.RawStdEncoding.EncodeToString(f.Key), + Cert: base64.RawStdEncoding.EncodeToString(f.Cert), + }, nil +} + +func (m *imlCertificate) Delete(ctx context.Context, id string) error { + cert, _, err := m.service.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil + } + return err + } + clusters, err := m.clusterService.ListByClusters(ctx, cert.Cluster) + if err != nil { + return err + } + return m.transaction.Transaction(ctx, func(ctx context.Context) error { + for _, c := range clusters { + err = m.syncGateway(ctx, c.Uuid, &gateway.DynamicRelease{ + BasicItem: &gateway.BasicItem{ + ID: id, + Description: "", + }, + }, false) + if err != nil { + return err + } + } + return m.service.Delete(ctx, id) + }) +} diff --git a/module/cluster/cluster.go b/module/cluster/cluster.go new file mode 100644 index 00000000..109cbd6d --- /dev/null +++ b/module/cluster/cluster.go @@ -0,0 +1,21 @@ +package cluster + +import ( + "context" + "reflect" + + cluster_dto "github.com/APIParkLab/APIPark/module/cluster/dto" + "github.com/eolinker/go-common/autowire" +) + +type IClusterModule interface { + CheckCluster(ctx context.Context, address ...string) ([]*cluster_dto.Node, error) + ResetCluster(ctx context.Context, clusterId string, address string) ([]*cluster_dto.Node, error) + ClusterNodes(ctx context.Context, clusterId string) ([]*cluster_dto.Node, error) +} + +func init() { + autowire.Auto[IClusterModule](func() reflect.Value { + return reflect.ValueOf(new(imlClusterModule)) + }) +} diff --git a/module/cluster/dto/input.go b/module/cluster/dto/input.go new file mode 100644 index 00000000..9d690526 --- /dev/null +++ b/module/cluster/dto/input.go @@ -0,0 +1,40 @@ +package cluster_dto + +//type Create struct { +// Id string `json:"id,omitempty"` +// Name string `json:"name,omitempty"` +// Description string `json:"description,omitempty"` +// Prefix string `json:"prefix,omitempty"` +// Url string `json:"url,omitempty"` +// ManagerAddress string `json:"manager_address,omitempty"` +//} +//type Edit struct { +// Name *string `json:"name,omitempty"` +// Description *string `json:"description,omitempty"` +// Prefix *string `json:"prefix,omitempty"` +// Url *string `json:"url,omitempty"` +//} + +//type SaveMonitorConfig struct { +// Driver string `json:"driver"` +// Config map[string]interface{} `json:"config"` +//} + +//type MonitorConfig struct { +// Driver string `json:"driver"` +// Config map[string]interface{} `json:"config"` +//} + +//type MonitorPartition struct { +// Id string `json:"id"` +// Name string `json:"name"` +// EnableMonitor bool `json:"enable_monitor"` +//} + +type ResetCluster struct { + ManagerAddress string `json:"manager_address"` +} + +type CheckCluster struct { + Address string `json:"address"` +} diff --git a/module/cluster/dto/output.go b/module/cluster/dto/output.go new file mode 100644 index 00000000..212c54e9 --- /dev/null +++ b/module/cluster/dto/output.go @@ -0,0 +1,51 @@ +package cluster_dto + +import ( + "github.com/eolinker/go-common/auto" +) + +type Item struct { + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + ClusterNum int `json:"cluster_num"` + CreateTime auto.TimeLabel `json:"create_time" ` + UpdateTime auto.TimeLabel `json:"update_time"` + Updater auto.Label `json:"updater" aolabel:"user"` + Creator auto.Label `json:"creator" aolabel:"user"` +} +type Simple struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` +} +type Cluster struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` +} +type SimpleWithCluster struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Clusters []*Cluster `json:"clusters,omitempty"` +} + +type Detail struct { + Updater auto.Label `json:"updater"` + Creator auto.Label `json:"creator"` + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Prefix string `json:"prefix,omitempty"` + CreateTime auto.TimeLabel `json:"create_time,omitempty"` + UpdateTime auto.TimeLabel `json:"update_time,omitempty"` + CanDelete bool `json:"can_delete"` +} + +type Node struct { + Id string `json:"id"` + Name string `json:"name"` + Admins []string `json:"manager_address"` + Peers []string `json:"peer_address"` + Gateways []string `json:"service_address"` + Status int `json:"status"` +} diff --git a/module/cluster/impl.go b/module/cluster/impl.go new file mode 100644 index 00000000..06d3b0af --- /dev/null +++ b/module/cluster/impl.go @@ -0,0 +1,300 @@ +package cluster + +import ( + "context" + + cluster_dto "github.com/APIParkLab/APIPark/module/cluster/dto" + + "github.com/APIParkLab/APIPark/gateway/admin" + "github.com/eolinker/eosc/log" + + "github.com/eolinker/go-common/store" + + "github.com/APIParkLab/APIPark/gateway" + + "github.com/APIParkLab/APIPark/service/cluster" + "github.com/eolinker/ap-account/service/account" + "github.com/eolinker/go-common/utils" +) + +var ( + _ IClusterModule = (*imlClusterModule)(nil) +) + +type imlClusterModule struct { + clusterService cluster.IClusterService `autowired:""` + userNameService account.IAccountService `autowired:""` + transaction store.ITransaction `autowired:""` +} + +func (m *imlClusterModule) CheckCluster(ctx context.Context, address ...string) ([]*cluster_dto.Node, error) { + info, err := admin.Admin(address...).Info(ctx) + if err != nil { + return nil, err + } + nodesOut := utils.SliceToSlice(info.Nodes, func(i *admin.Node) *cluster_dto.Node { + return &cluster_dto.Node{ + Id: i.Id, + Name: i.Name, + Admins: i.Admin, + Peers: i.Peer, + Gateways: i.Server, + } + }) + nodeStatus(ctx, nodesOut) + + return nodesOut, nil +} + +func (m *imlClusterModule) ResetCluster(ctx context.Context, clusterId string, address string) ([]*cluster_dto.Node, error) { + + nodes, err := m.clusterService.UpdateAddress(ctx, clusterId, address) + if err != nil { + return nil, err + } + err = m.initGateway(ctx, clusterId) + if err != nil { + return nil, err + } + nodesOut := utils.SliceToSlice(nodes, func(i *cluster.Node) *cluster_dto.Node { + return &cluster_dto.Node{ + Id: i.Uuid, + Name: i.Name, + Admins: i.Admin, + Peers: i.Peer, + Gateways: i.Server, + } + }) + + nodeStatus(ctx, nodesOut) + return nodesOut, nil +} +func (m *imlClusterModule) initGateway(ctx context.Context, clusterId string) error { + client, err := m.clusterService.GatewayClient(ctx, clusterId) + if err != nil { + return err + } + defer func() { + err := client.Close(ctx) + if err != nil { + log.Warn("close apinto client:", err) + } + }() + return gateway.InitGateway(ctx, clusterId, client) +} +func (m *imlClusterModule) ClusterNodes(ctx context.Context, clusterId string) ([]*cluster_dto.Node, error) { + + nodes, err := m.clusterService.Nodes(ctx) + if err != nil { + return nil, err + } + nodesOut := utils.SliceToSlice(nodes, func(i *cluster.Node) *cluster_dto.Node { + return &cluster_dto.Node{ + Id: i.Uuid, + Name: i.Name, + Admins: i.Admin, + Peers: i.Peer, + Gateways: i.Server, + } + }) + nodeStatus(ctx, nodesOut) + + return nodesOut, nil +} + +// +//func (m *imlClusterModule) CreatePartition(ctx context.Context, create *paritiondto.Create) (*paritiondto.Detail, error) { +// if create.Id == "" { +// create.Id = uuid.New().String() +// } +// if create.Name == "" { +// return nil, errors.New("name is empty") +// } +// clusterId := "" +// err := m.transaction.Transaction(ctx, func(ctx context.Context) error { +// clusterInfo, err := m.clusterService.Create(ctx, create.Id, create.Id, create.Description, create.ManagerAddress) +// if err != nil { +// return err +// } +// if create.Prefix != "" { +// create.Prefix = fmt.Sprintf("/%s", strings.TrimPrefix(create.Prefix, "/")) +// } +// clusterId = clusterInfo.Uuid +// return m.partitionService.Create(ctx, &partition.CreatePartition{ +// Uuid: create.Id, +// Name: create.Name, +// Resume: create.Description, +// Prefix: create.Prefix, +// Url: create.Url, +// Cluster: clusterInfo.Uuid, +// }) +// }) +// if err != nil { +// return nil, err +// } +// err = m.initGateway(ctx, create.Id, clusterId) +// if err != nil { +// return nil, err +// } +// return m.Get(ctx, create.Id) +//} +// +//func (m *imlClusterModule) Search(ctx context.Context, keyword string) ([]*paritiondto.Item, error) { +// partitions, err := m.partitionService.Search(ctx, keyword, nil) +// if err != nil { +// return nil, err +// } +// countMap, err := m.clusterService.CountByPartition(ctx) +// if err != nil { +// return nil, err +// } +// items := utils.SliceToSlice(partitions, func(i *partition.Cluster) *paritiondto.Item { +// +// return &paritiondto.Item{ +// Creator: auto.UUID(i.Creator), +// Updater: auto.UUID(i.Updater), +// Id: i.UUID, +// Name: i.Name, +// Description: i.Resume, +// ClusterNum: countMap[i.UUID], +// CreateTime: auto.TimeLabel(i.CreateTime), +// UpdateTime: auto.TimeLabel(i.UpdateTime), +// } +// }) +// if len(items) > 0 { +// counts, err := m.clusterService.CountByPartition(ctx) +// if err != nil { +// return nil, err +// } +// for _, item := range items { +// item.ClusterNum = counts[item.Id] +// } +// } +// +// return items, nil +//} +// +//func (m *imlClusterModule) Get(ctx context.Context, id string) (*paritiondto.Detail, error) { +// pm, err := m.partitionService.Get(ctx, id) +// if err != nil { +// return nil, err +// } +// //oDetails, err := m.organizationService.Search(ctx, "") +// //if err != nil { +// // return nil, err +// //} +// //canDelete := true +// //for _, o := range oDetails { +// // for _, p := range o.Clusters { +// // if p == id { +// // canDelete = false +// // break +// // } +// // } +// // if !canDelete { +// // break +// // } +// //} +// +// pd := &paritiondto.Detail{ +// Creator: auto.UUID(pm.Creator), +// Updater: auto.UUID(pm.Updater), +// Id: pm.UUID, +// Name: pm.Name, +// Description: pm.Resume, +// Prefix: pm.Prefix, +// CreateTime: auto.TimeLabel(pm.CreateTime), +// UpdateTime: auto.TimeLabel(pm.UpdateTime), +// //CanDelete: canDelete, +// } +// return pd, nil +//} +// +//func (m *imlClusterModule) Update(ctx context.Context, id string, edit *paritiondto.Edit) (*paritiondto.Detail, error) { +// err := m.partitionService.Save(ctx, id, &partition.EditPartition{ +// Name: edit.Name, +// Resume: edit.Description, +// Prefix: edit.Prefix, +// Url: edit.Url, +// }) +// if err != nil { +// return nil, err +// } +// return m.Get(ctx, id) +//} +// +//func (m *imlClusterModule) Delete(ctx context.Context, id string) error { +// return m.transaction.Transaction(ctx, func(ctx context.Context) error { +// info, err := m.partitionService.Get(ctx, id) +// if err != nil { +// if errors.Is(err, gorm.ErrRecordNotFound) { +// return nil +// } +// return err +// } +// err = m.clusterService.Delete(ctx, info.Cluster) +// if err != nil { +// return err +// } +// return m.partitionService.Delete(ctx, id) +// }) +// +//} +// +//func (m *imlClusterModule) Simple(ctx context.Context) ([]*paritiondto.Simple, error) { +// pm, err := m.partitionService.Search(ctx, "", nil) +// if err != nil { +// return nil, err +// } +// pd := utils.SliceToSlice(pm, func(i *partition.Cluster) *paritiondto.Simple { +// return &paritiondto.Simple{ +// Id: i.UUID, +// Name: i.Name, +// } +// }) +// return pd, nil +//} +// +//func (m *imlClusterModule) SimpleByIds(ctx context.Context, ids []string) ([]*paritiondto.Simple, error) { +// pm, err := m.partitionService.Search(ctx, "", map[string]interface{}{ +// "uuid": ids, +// }) +// if err != nil { +// return nil, err +// } +// pd := utils.SliceToSlice(pm, func(i *partition.Cluster) *paritiondto.Simple { +// return &paritiondto.Simple{ +// Id: i.UUID, +// Name: i.Name, +// } +// }) +// return pd, nil +// +//} +//func (m *imlClusterModule) SimpleWithCluster(ctx context.Context) ([]*paritiondto.SimpleWithCluster, error) { +// pm, err := m.partitionService.Search(ctx, "", nil) +// if err != nil { +// return nil, err +// } +// +// clusterList, err := m.clusterService.List(ctx) +// if err != nil { +// return nil, err +// } +// +// clusterMap := utils.SliceToMapArrayO(clusterList, func(i *cluster.Cluster) (string, *paritiondto.Cluster) { +// return i.Cluster, &paritiondto.Cluster{ +// Id: i.Uuid, +// Name: i.Name, +// Description: i.Resume, +// } +// }) +// pd := utils.SliceToSlice(pm, func(i *partition.Cluster) *paritiondto.SimpleWithCluster { +// return &paritiondto.SimpleWithCluster{ +// Id: i.UUID, +// Name: i.Name, +// Clusters: clusterMap[i.UUID], +// } +// }) +// return pd, nil +//} diff --git a/module/cluster/util.go b/module/cluster/util.go new file mode 100644 index 00000000..08bf5c83 --- /dev/null +++ b/module/cluster/util.go @@ -0,0 +1,43 @@ +package cluster + +import ( + "context" + "sync" + + cluster_dto "github.com/APIParkLab/APIPark/module/cluster/dto" + + "github.com/APIParkLab/APIPark/gateway/admin" +) + +func nodeStatus(ctx context.Context, nodes []*cluster_dto.Node) { + if len(nodes) == 0 { + return + } + + if len(nodes) == 1 { + nodes[0].Status = ping(ctx, nodes[0].Admins...) + } + + wg := sync.WaitGroup{} + wg.Add(len(nodes)) + + for _, n := range nodes { + go doPingRouting(ctx, n, &wg) + } + wg.Wait() +} +func doPingRouting(ctx context.Context, n *cluster_dto.Node, wg *sync.WaitGroup) { + n.Status = ping(ctx, n.Admins...) + wg.Done() +} +func ping(ctx context.Context, address ...string) int { + if len(address) == 0 { + return 0 + } + err := admin.Admin(address...).Ping(ctx) + if err != nil { + return 0 + } + + return 1 +} diff --git a/module/dynamic-module/driver/driver.go b/module/dynamic-module/driver/driver.go new file mode 100644 index 00000000..0a6d4f6f --- /dev/null +++ b/module/dynamic-module/driver/driver.go @@ -0,0 +1,137 @@ +package driver + +import "encoding/json" + +type IDriver interface { + ID() string + Name() string + Title() string + Group() string + Front() string + Define() IDefine +} + +type IDefine interface { + Profession() string + Skill() string + Drivers() []*Field + Fields(fields ...*Field) []*Field + Render() map[string]interface{} + Columns() []string +} + +type Driver struct { + id string + name string + title string + group string + front string + define IDefine +} + +func (d *Driver) ID() string { + return d.id +} + +func (d *Driver) Name() string { + return d.name +} + +func (d *Driver) Title() string { + return d.title +} + +func (d *Driver) Group() string { + return d.group +} + +func (d *Driver) Front() string { + return d.front +} + +func (d *Driver) Define() IDefine { + return d.define +} + +func NewDriver(cfg *PluginCfg) IDriver { + return &Driver{ + id: cfg.Id, + name: cfg.Name, + title: cfg.Cname, + group: cfg.GroupId, + front: cfg.Front, + define: NewDefine(cfg.Define), + } +} + +var defaultFields = []*Field{ + { + Name: "updater", + Title: "更新者", + }, + { + Name: "update_time", + Title: "更新时间", + }, +} + +type Define struct { + profession string + skill string + drivers []*Field + fields []*Field + columns []string + render map[string]interface{} +} + +func (d *Define) Columns() []string { + return d.columns +} + +func NewDefine(d *PluginDefine) *Define { + columns := make([]string, 0, len(d.Fields)) + for _, field := range d.Fields { + columns = append(columns, field.Name) + } + render := make(map[string]interface{}) + for k, v := range d.Render { + r := make(map[string]interface{}) + err := json.Unmarshal([]byte(v), &r) + if err != nil { + continue + } + render[k] = r + } + return &Define{ + profession: d.Profession, + skill: d.Skill, + drivers: d.Drivers, + fields: d.Fields, + render: render, + columns: columns, + } +} + +func (d *Define) Profession() string { + return d.profession +} + +func (d *Define) Skill() string { + return d.skill +} + +func (d *Define) Drivers() []*Field { + return d.drivers +} + +func (d *Define) Fields(fields ...*Field) []*Field { + fs := make([]*Field, 0, len(d.fields)+len(fields)+len(defaultFields)) + fs = append(fs, d.fields...) + fs = append(fs, fields...) + fs = append(fs, defaultFields...) + return fs +} + +func (d *Define) Render() map[string]interface{} { + return d.render +} diff --git a/module/dynamic-module/driver/embed/file-access-log/README.md b/module/dynamic-module/driver/embed/file-access-log/README.md new file mode 100644 index 00000000..3dd6509d --- /dev/null +++ b/module/dynamic-module/driver/embed/file-access-log/README.md @@ -0,0 +1,141 @@ +# 文件日志 + +## 基本介绍 + +日志是用来暴露系统内部状态的一种手段,好的日志可以帮助开发人员快速定位问题所在,然后找到合适的方式解决掉问题。该插件支持将`节点访问日志`输出到`文件`中。 + +## 功能特性 + +文件日志:将请求信息输出到日志文件中,具备以下特性: + +- 自定义文件的存放目录及文件名称 +- 按照一定周期分割日志文件,避免单个文件过大不好查看的问题 +- 定时删除过期文件,降低硬盘空间开销 + +可配合控制台**日志检索**插件使用,在控制台中追踪节点请求日志,并且可以下载历史日志。 + +## 功能演示 + +### 新建文件日志配置 + +1、点击左侧导航栏`文件日志`,进入文件日志列表页面,点击`新建文件日志` + +![](http://data.eolinker.com/course/Hyej4cd6edbb5520f7f62618145d4dca056ee11ae1c3bde.png) + +2、填写文件日志配置 + +![](http://data.eolinker.com/course/VPAFJkz46df9dbc795aeab9afe5e8d6210cca03af46b63d.png) + +**配置说明**: + +| 字段名称 | 说明 | +| :----------- | :----------------------------------------------------------- | +| 文件名称 | 存放的文件名称,实际存放的名称会加上 `.log` 后缀,即为:{文件名称}.log | +| 存放目录 | 文件存放目录,支持相对路径和绝对路径 | +| 日志分割周期 | 按照一定周期创建新日志文件,旧日志文件将会重命名,可选项:小时、天 | +| 过期时间 | 文件保存时间,单位:天,超过保存时间的,将定时清理删除 | +| 输出格式 | 输出日志内容格式,支持单行、Json格式输出 | +| 格式化配置 | 输出格式模版,配置教程[点此](https://help.apinto.com/docs/formatter)进行跳转 | + +**文件生命周期演示** + + + +![img](http://data.eolinker.com/course/tgLQMA27ce951803c9e4c6ab3c82a899863c86f86624e01.png) + + + +**示例格式化配置** + +``` +{ + "fields": [ + "$time_iso8601", + "$request_id", + "@request", + "@proxy", + "@response", + "@status_code", + "@time" + ], + "request": [ + "$request_method", + "$scheme", + "$request_uri", + "$host", + "$header", + "$remote_addr" + ], + "proxy": [ + "$proxy_method", + "$proxy_scheme", + "$proxy_uri", + "$proxy_host", + "$proxy_header", + "$proxy_addr" + ], + "response": [ + "$response_header" + ], + "status_code": [ + "$status", + "$proxy_status" + ], + "time": [ + "$request_time", + "$response_time" + ] +} +``` + +3、点击确定后,日志输出添加完成 + + + +![img](http://data.eolinker.com/course/GXFbedia2b05c6b0ce77da8a38f536160af4ec11e1209cf.png) + +### 发布到集群 + +1、点击列表右侧`小飞机`按钮,将日志输出配置发布上线 + + + +![img](http://data.eolinker.com/course/gxDIv7z9cc0f9e18b0e905f0e8f185a958f3c5c8d25e6a8.png) + + + +2、选择其中需要发布上线的环境,点击`上线` + + + +![img](http://data.eolinker.com/course/cXAzeC7c3391a55bec8eb5be6c0c6baf2baf3226d9c31e9.png)x + + + +3、上线成功后,列表会实时显示相应集群的发布状态 + + + +![img](http://data.eolinker.com/course/n6vc56D488d01bdf61f85e12507117546806602ea0f380f.png) + +### 访问接口,打印日志输出 + +访问在网关上上线的接口,此处使用`Apikit`的测试功能进行演示 + + + +![img](http://data.eolinker.com/course/l2sHmd3600aeebb248a48629498f4a0ab9e2529ac1e3587.png) + + + +访问完成后,进入节点目录,查看access日志输出信息,如下图 + + + +![img](http://data.eolinker.com/course/d5ryFin9e200c902beea742b311944041249ce19732bb28.png) + +## 更新日志 + +### V1.0(2023-6-19) + +- 插件上线 \ No newline at end of file diff --git a/module/dynamic-module/driver/embed/file-access-log/plugin.yml b/module/dynamic-module/driver/embed/file-access-log/plugin.yml new file mode 100644 index 00000000..2d950e1d --- /dev/null +++ b/module/dynamic-module/driver/embed/file-access-log/plugin.yml @@ -0,0 +1,229 @@ +#file: noinspection YAMLSchemaValidation +id: "file-access-log.apinto.com" +name: "file-access-log" +cname: "文件日志" +resume: "将请求和响应数据输出到日志文件中" +version: "v1.0.0" +icon: "文件日志.png" +driver: "dynamic.apinto.com" +front: template/file-access-log +navigation: "navigation.system" +group_id: "log" +frontend: + - name: file-access-log + driver: apinto.intelligent.normal + router: + - path: template/file-access-log + type: normal +define: # 动态模块定义 + profession: output + drivers: + - name: file + title: 文件 + skill: Access-Output + fields: + - name: title # 定义从响应中对应字段中获取显示值 + title: 名称 + - name: id + title: ID + - name: driver + title: 驱动名称 + - name: description + title: 描述 + render: + file: | + { + "type":"object", + "properties":{ + "scopes":{ + "type":"array", + "title":"作用范围", + "x-decorator":"FormItem", + "x-component":"ArrayItems", + "x-decorator-props":{ + "labelCol":6, + "wrapperCol":10 + }, + "items":{ + "type":"void", + "x-component":"Space", + "properties":{ + "sort":{ + "type":"void", + "x-decorator":"FormItem", + "x-component":"ArrayItems.SortHandle" + }, + "select":{ + "type":"string", + "x-decorator":"FormItem", + "x-component":"Select", + "enum":[ + { + "label":"Access日志", + "value":"access_log" + } + ] + }, + "remove":{ + "type":"void", + "x-decorator":"FormItem", + "x-component":"ArrayItems.Remove" + } + } + }, + "properties":{ + "add":{ + "type":"void", + "title":"添加条目", + "x-component":"ArrayItems.Addition", + "x-component-props":{ + "defaultValue":"access_log" + } + } + }, + "name":"scopes", + "x-index":0, + "required":true + }, + "file":{ + "type":"string", + "title":"文件名称", + "x-decorator":"FormItem", + "x-component":"Input", + "x-validator":[ + + ], + "x-component-props":{ + + }, + "x-decorator-props":{ + "labelCol":6, + "wrapperCol":10 + }, + "name":"file", + "x-index":1, + "required":true + }, + "dir":{ + "type":"string", + "title":"存放目录", + "x-decorator":"FormItem", + "x-component":"Input", + "x-validator":[ + + ], + "x-component-props":{ + + }, + "x-decorator-props":{ + "labelCol":6, + "wrapperCol":10 + }, + "name":"dir", + "x-index":2, + "required":true + }, + "period":{ + "title":"日志分割周期", + "x-decorator":"FormItem", + "x-component":"Select", + "x-validator":[ + + ], + "x-component-props":{ + + }, + "x-decorator-props":{ + "labelCol":6, + "wrapperCol":10 + }, + "enum":[ + { + "children":[ + + ], + "label":"小时", + "value":"hour" + }, + { + "children":[ + + ], + "label":"天", + "value":"day" + } + ], + "default":"hour", + "name":"period", + "x-index":3, + "required":true + }, + "expore":{ + "type":"number", + "title":"过期时间", + "x-decorator":"FormItem", + "x-component":"NumberPicker", + "x-validator":"integer", + "x-component-props":{ + + }, + "x-decorator-props":{ + "labelCol":6, + "wrapperCol":10 + }, + "name":"expore", + "x-index":4, + "default":"3", + "description":"单位:天", + "required":true + }, + "type":{ + "title":"输出格式", + "x-decorator":"FormItem", + "x-component":"Select", + "x-validator":[ + + ], + "x-component-props":{ + + }, + "x-decorator-props":{ + "labelCol":6, + "wrapperCol":10 + }, + "enum":[ + { + "children":[ + + ], + "label":"单行", + "value":"line" + }, + { + "children":[ + + ], + "label":"Json", + "value":"json" + } + ], + "default":"line", + "name":"type", + "x-index":5, + "required":true + }, + "formatter":{ + "type":"object", + "title":"格式化配置", + "x-decorator":"FormItem", + "x-component":"CustomCodeboxComponent", + "x-component-props":{ + "mode":"json" + }, + "x-decorator-props":{ + "labelCol":6, + "wrapperCol":10 + } + } + } + } \ No newline at end of file diff --git a/module/dynamic-module/driver/embed/file-access-log/文件日志.png b/module/dynamic-module/driver/embed/file-access-log/文件日志.png new file mode 100644 index 00000000..57ab9af5 Binary files /dev/null and b/module/dynamic-module/driver/embed/file-access-log/文件日志.png differ diff --git a/module/dynamic-module/driver/embed/http-access-log/HTTP日志.png b/module/dynamic-module/driver/embed/http-access-log/HTTP日志.png new file mode 100644 index 00000000..678feab7 Binary files /dev/null and b/module/dynamic-module/driver/embed/http-access-log/HTTP日志.png differ diff --git a/module/dynamic-module/driver/embed/http-access-log/README.md b/module/dynamic-module/driver/embed/http-access-log/README.md new file mode 100644 index 00000000..8824e9be --- /dev/null +++ b/module/dynamic-module/driver/embed/http-access-log/README.md @@ -0,0 +1,103 @@ +# HTTP日志 + +## 基本介绍 + +日志是用来暴露系统内部状态的一种手段,好的日志可以帮助开发人员快速定位问题所在,然后找到合适的方式解决掉问题。该插件支持将`节点访问日志`输出到**HTTP服务器**中。 + +## 功能特性 + +HTTP日志插件通过HTTP请求的方式,将节点访问日志发送给HTTP服务接口中,并且具备以下特性: + +* 支持多种请求方式,包括**POST**、**PUT**、**PATCH** +* 支持自定义请求头部 +* 支持日志输出格式类型 +* 支持自定义日志格式化配置 + +## 功能演示 + +### 新建HTTP日志配置 + +1、点击左侧导航栏`系统管理` -> `HTTP日志`,进入 `HTTP日志`列表页面,点击`新建HTTP日志` + +![](http://data.eolinker.com/course/RG9NpXfd8506f189f6cc37567b943aaef81fbe98094e511.png) + +2、填写HTTP日志配置 + +![](http://data.eolinker.com/course/1Y4YJLD0edb4c6ed4fa197529245e4c841ae049e5564fb7.png) + +**配置说明**: + +| 字段名称 | 说明 | +| :--------- | :----------------------------------------------------------- | +| 请求方式 | 请求HTTP服务接口时使用的请求方式,目前支持POST、PUT、PATCH | +| URL | HTTP服务接口的完整请求路径 | +| 请求头部 | 请求的头部信息,可以填请求HTTP服务接口时需要提供的参数,如鉴权等信息
填写时,需要填写JSON格式数据,数据为`key-value`格式,如:
`{"from":"apinto"}` | +| 输出格式 | 输出日志内容格式,支持单行、Json格式输出 | +| 格式化配置 | 输出格式模版,配置教程[点此](https://help.apinto.com/docs/formatter)进行跳转 | + +**示例格式化配置** + +``` +{ + "fields": [ + "$time_iso8601", + "$request_id", + "@request", + "@proxy", + "@response", + "@status_code", + "@time" + ], + "request": [ + "$request_method", + "$scheme", + "$request_uri", + "$host", + "$header", + "$remote_addr" + ], + "proxy": [ + "$proxy_method", + "$proxy_scheme", + "$proxy_uri", + "$proxy_host", + "$proxy_header", + "$proxy_addr" + ], + "response": [ + "$response_header" + ], + "status_code": [ + "$status", + "$proxy_status" + ], + "time": [ + "$request_time", + "$response_time" + ] +} +``` + +3、点击确定后,HTTP日志添加完成 + +![](http://data.eolinker.com/course/v3wls8u9bae38349a42610d185250ee7c2134243ccb5a40.png) + +### 发布到集群 + +1、点击列表右侧`小飞机`按钮,将HTTP日志配置发布上线 + +![](http://data.eolinker.com/course/7m9Wh711763756ff29bd43ac52cc2e847de5daa33b1a848.png) + +2、选择其中需要发布上线的环境,点击`上线` + +![](http://data.eolinker.com/course/cJjq55Cc14c110ac9e84d9e427cf8e1af0a182689c09cd7.png) + +3、上线成功后,列表会实时显示相应集群的发布状态 + +![](http://data.eolinker.com/course/Rh7wxRW0860282202bf48d968433581062243d8ed6b9055.png) + +## 更新日志 + +### V1.0(2023-6-19) + +- 插件上线 \ No newline at end of file diff --git a/module/dynamic-module/driver/embed/http-access-log/plugin.yml b/module/dynamic-module/driver/embed/http-access-log/plugin.yml new file mode 100644 index 00000000..fd184d65 --- /dev/null +++ b/module/dynamic-module/driver/embed/http-access-log/plugin.yml @@ -0,0 +1,203 @@ +id: "http-access-log.apinto.com" +name: "http-access-log" +cname: "HTTP日志" +resume: "将请求和响应日志发送到HTTP服务器" +version: "v1.0.0" +icon: "HTTP日志.png" +driver: "dynamic.apinto.com" +front: template/http-access-log +navigation: "navigation.system" +group_id: "log" +frontend: + - name: http-access-log + driver: apinto.intelligent.normal + router: + - path: template/http-access-log + type: normal +define: # 动态模块定义 + profession: output + drivers: + - name: http_output + title: http请求 + skill: Access-Output + fields: + - name: title # 定义从响应中对应字段中获取显示值 + title: 名称 + - name: id + title: ID + - name: driver + title: 驱动名称 + - name: description + title: 描述 + render: + http_output: | + { + "type": "object", + "properties": { + "scopes": { + "type": "array", + "title": "作用范围", + "x-decorator": "FormItem", + "x-component": "ArrayItems", + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "scopes", + "x-index": 0, + "required": true, + "items": { + "type": "void", + "x-component": "Space", + "properties": { + "sort": { + "type": "void", + "x-decorator": "FormItem", + "x-component": "ArrayItems.SortHandle", + "name": "sort", + "x-index": 0 + }, + "select": { + "type": "string", + "x-decorator": "FormItem", + "x-component": "Select", + "enum": [ + { + "label": "Access日志", + "value": "access_log" + } + ], + "name": "select", + "x-index": 1 + }, + "remove": { + "type": "void", + "x-decorator": "FormItem", + "x-component": "ArrayItems.Remove", + "name": "remove", + "x-index": 2 + } + } + }, + "properties": { + "add": { + "type": "void", + "title": "添加条目", + "x-component": "ArrayItems.Addition", + "x-component-props": { + "defaultValue": "access_log" + }, + "name": "add", + "x-index": 0 + } + } + }, + "method": { + "type": "string", + "title": "请求方式", + "x-decorator": "FormItem", + "x-component": "Select", + "x-validator": [], + "x-component-props": {}, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "enum": [ + { + "label": "POST", + "value": "POST" + }, + { + "label": "PUT", + "value": "PUT" + }, + { + "label": "PATCH", + "value": "PATCH" + } + ], + "default": "POST", + "name": "method", + "x-index": 1, + "required": true + }, + "url": { + "type": "string", + "title": "URL", + "x-decorator": "FormItem", + "x-component": "Input", + "x-validator": [ + { + "triggerType": "onBlur", + "pattern": "^[a-zA-z]+://[^\\s]*$" + } + ], + "x-component-props": {}, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "url", + "x-index": 2, + "required": true + }, + "headers": { + "type": "object", + "title": "请求头部", + "x-decorator": "FormItem", + "x-component": "CustomCodeboxComponent", + "x-component-props": { + "mode": "json" + }, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "headers", + "x-index": 3 + }, + "type": { + "title": "输出格式", + "x-decorator": "FormItem", + "x-component": "Select", + "x-validator": [], + "x-component-props": {}, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "enum": [ + { + "children": [], + "label": "单行", + "value": "line" + }, + { + "children": [], + "label": "Json", + "value": "json" + } + ], + "default": "line", + "name": "type", + "x-index": 4, + "required": true + }, + "formatter": { + "type": "object", + "title": "格式化配置", + "x-decorator": "FormItem", + "x-component": "CustomCodeboxComponent", + "x-component-props": { + "mode": "json" + }, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "formatter", + "x-index": 5 + } + } + } \ No newline at end of file diff --git a/module/dynamic-module/driver/embed/kafka-access-log/Kafka日志.png b/module/dynamic-module/driver/embed/kafka-access-log/Kafka日志.png new file mode 100644 index 00000000..e381aabd Binary files /dev/null and b/module/dynamic-module/driver/embed/kafka-access-log/Kafka日志.png differ diff --git a/module/dynamic-module/driver/embed/kafka-access-log/README.md b/module/dynamic-module/driver/embed/kafka-access-log/README.md new file mode 100644 index 00000000..7831db09 --- /dev/null +++ b/module/dynamic-module/driver/embed/kafka-access-log/README.md @@ -0,0 +1,107 @@ +# Kafka日志 + +## 基本介绍 + +日志是用来暴露系统内部状态的一种手段,好的日志可以帮助开发人员快速定位问题所在,然后找到合适的方式解决掉问题。该插件支持将`节点访问日志`输出到`Kafka`中。 + +## 功能特性 + +Kafka日志:能够将程序运行中产生的日志内容输出到指定Kafka集群队列中。 + +* 支持多种请求协议,包括TCP、UDP、UNIX +* 支持设置Syslog输出日志等级 +* 支持日志输出格式类型 +* 支持自定义日志格式化配置 + +## 功能演示 + +### 新建Kafka日志配置 + +1、点击左侧导航栏`系统管理` -> `Kafka日志`,进入 `Kafka日志`列表页面,点击`新建Kafka日志` + +![](http://data.eolinker.com/course/XgzgbwP536980f85f2d1367925bc1f9b7da60f1be9702c6.png) + +2、填写Kafka日志配置 + +![](http://data.eolinker.com/course/1HAdPXZa7695b5c5b14e3755885b0586fae27235e4361cf.png) + +**配置说明**: + +| 字段名称 | 说明 | +| :------------- | :----------------------------------------------------------- | +| 版本 | Kafka版本 | +| 服务器地址 | Kafka服务地址,多个地址用英文逗号分隔 | +| Topic | Kafka服务Topic信息 | +| Partition Type | partition的选择方式,默认采用hash,选择hash时,若partition_key为空,则采用随机选择random | +| Partition | Partition Type为manual时,该项指定分区号 | +| Partition Key | Partition Type为hash时,该项指定hash值 | +| 请求超时时间 | 超时时间,单位为second | +| 输出格式 | 输出日志内容格式,支持单行、Json格式输出 | +| 格式化配置 | 输出格式模版,配置教程[点此](https://help.apinto.com/docs/formatter)进行跳转 | + +**示例格式化配置** + +``` +{ + "fields": [ + "$time_iso8601", + "$request_id", + "@request", + "@proxy", + "@response", + "@status_code", + "@time" + ], + "request": [ + "$request_method", + "$scheme", + "$request_uri", + "$host", + "$header", + "$remote_addr" + ], + "proxy": [ + "$proxy_method", + "$proxy_scheme", + "$proxy_uri", + "$proxy_host", + "$proxy_header", + "$proxy_addr" + ], + "response": [ + "$response_header" + ], + "status_code": [ + "$status", + "$proxy_status" + ], + "time": [ + "$request_time", + "$response_time" + ] +} +``` + +3、点击确定后,Kafka日志添加完成 + +![](http://data.eolinker.com/course/v6vFVL66cf158a77aa4f483d5f76dbfb3726ca4f9971fd9.png) + +### 发布到集群 + +1、点击列表右侧`小飞机`按钮,将Kafka日志配置发布上线 + +![](http://data.eolinker.com/course/JvlasZie74489a4ace107a87da0e4971df3511d41869999.png) + +2、选择其中需要发布上线的环境,点击`上线` + +![](http://data.eolinker.com/course/UU1UCzcb6b816a309e2bbe3a9b3428c1628abe8284daf06.png) + +3、上线成功后,列表会实时显示相应集群的发布状态 + +![](http://data.eolinker.com/course/W9p28rR1cd161e6b9219834a45e113d75077b45a09c9cfa.png) + +## 更新日志 + +### V1.0(2023-6-19) + +- 插件上线 \ No newline at end of file diff --git a/module/dynamic-module/driver/embed/kafka-access-log/plugin.yml b/module/dynamic-module/driver/embed/kafka-access-log/plugin.yml new file mode 100644 index 00000000..f5dbda89 --- /dev/null +++ b/module/dynamic-module/driver/embed/kafka-access-log/plugin.yml @@ -0,0 +1,527 @@ +id: "kafka-access-log.apinto.com" +name: "kafka-access-log" +cname: "Kafka日志" +resume: "将请求和响应日志发布到Apache Kafka topic中" +version: "v1.0.0" +icon: "Kafka日志.png" +driver: "dynamic.apinto.com" +front: template/kafka-access-log +navigation: "navigation.system" +group_id: "log" +frontend: + - name: kafka-access-log + driver: apinto.intelligent.normal + router: + - path: template/kafka-access-log + type: normal +define: # 动态模块定义 + profession: output + drivers: + - name: kafka_output + title: Kafka + skill: Access-Output + fields: + - name: title # 定义从响应中对应字段中获取显示值 + title: 名称 + - name: id + title: ID + - name: driver + title: 驱动名称 + - name: description + title: 描述 + render: + kafka_output: | + { + "type": "object", + "properties": { + "scopes": { + "type": "array", + "title": "作用范围", + "x-decorator": "FormItem", + "x-component": "ArrayItems", + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "scopes", + "x-index": 0, + "required": true, + "x-designable-id": "14ambdfgkyl", + "items": { + "type": "void", + "x-component": "Space", + "x-designable-id": "5dwv836plg8", + "properties": { + "sort": { + "type": "void", + "x-decorator": "FormItem", + "x-component": "ArrayItems.SortHandle", + "name": "sort", + "x-index": 0, + "x-designable-id": "vj263v9oh37" + }, + "select": { + "type": "string", + "x-decorator": "FormItem", + "x-component": "Select", + "enum": [ + { + "label": "Access日志", + "value": "access_log" + } + ], + "name": "select", + "x-index": 1, + "x-designable-id": "j2vu3wd3cu8" + }, + "remove": { + "type": "void", + "x-decorator": "FormItem", + "x-component": "ArrayItems.Remove", + "name": "remove", + "x-index": 2, + "x-designable-id": "p4ieu9yteew" + } + } + }, + "properties": { + "add": { + "type": "void", + "title": "添加条目", + "x-component": "ArrayItems.Addition", + "x-component-props": { + "defaultValue": "access_log" + }, + "name": "add", + "x-index": 0, + "x-designable-id": "mfvpuzu4ma3" + } + } + }, + "kafka_version": { + "title": "版本", + "x-decorator": "FormItem", + "x-component": "Select", + "x-validator": [], + "x-component-props": {}, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "kafka_version", + "default": "3.1.0", + "required": true, + "enum": [ + { + "children": [], + "label": "3.1.0", + "value": "3.1.0" + }, + { + "children": [], + "label": "3.0.0", + "value": "3.0.0" + }, + { + "children": [], + "label": "2.8.1", + "value": "2.8.1" + }, + { + "children": [], + "label": "2.8.0", + "value": "2.8.0" + }, + { + "children": [], + "label": "2.7.1", + "value": "2.7.1" + }, + { + "children": [], + "label": "2.7.0", + "value": "2.7.0" + }, + { + "children": [], + "label": "2.6.2", + "value": "2.6.2" + }, + { + "children": [], + "label": "2.6.1", + "value": "2.6.1" + }, + { + "children": [], + "label": "2.6.0", + "value": "2.6.0" + }, + { + "children": [], + "label": "2.5.1", + "value": "2.5.1" + }, + { + "children": [], + "label": "2.5.0", + "value": "2.5.0" + }, + { + "children": [], + "label": "2.4.1", + "value": "2.4.1" + }, + { + "children": [], + "label": "2.4.0", + "value": "2.4.0" + }, + { + "children": [], + "label": "2.3.1", + "value": "2.3.1" + }, + { + "children": [], + "label": "2.3.0", + "value": "2.3.0" + }, + { + "children": [], + "label": "2.2.2", + "value": "2.2.2" + }, + { + "children": [], + "label": "2.2.1", + "value": "2.2.1" + }, + { + "children": [], + "label": "2.2.0", + "value": "2.2.0" + }, + { + "children": [], + "label": "2.1.1", + "value": "2.1.1" + }, + { + "children": [], + "label": "2.1.0", + "value": "2.1.0" + }, + { + "children": [], + "label": "2.0.1", + "value": "2.0.1" + }, + { + "children": [], + "label": "2.0.0", + "value": "2.0.0" + }, + { + "children": [], + "label": "1.1.1", + "value": "1.1.1" + }, + { + "children": [], + "label": "1.1.0", + "value": "1.1.0" + }, + { + "children": [], + "label": "1.0.2", + "value": "1.0.2" + }, + { + "children": [], + "label": "1.0.1", + "value": "1.0.1" + }, + { + "children": [], + "label": "1.0.0", + "value": "1.0.0" + }, + { + "children": [], + "label": "0.11.0.2", + "value": "0.11.0.2" + }, + { + "children": [], + "label": "0.11.0.1", + "value": "0.11.0.1" + }, + { + "children": [], + "label": "0.11.0.0", + "value": "0.11.0.0" + }, + { + "children": [], + "label": "0.10.2.2", + "value": "0.10.2.2" + }, + { + "children": [], + "label": "0.10.2.1", + "value": "0.10.2.1" + }, + { + "children": [], + "label": "0.10.2.0", + "value": "0.10.2.0" + }, + { + "children": [], + "label": "0.10.1.1", + "value": "0.10.1.1" + }, + { + "children": [], + "label": "0.10.1.0", + "value": "0.10.1.0" + }, + { + "children": [], + "label": "0.10.0.1", + "value": "0.10.0.1" + }, + { + "children": [], + "label": "0.10.0.0", + "value": "0.10.0.0" + }, + { + "children": [], + "label": "0.9.0.1", + "value": "0.9.0.1" + }, + { + "children": [], + "label": "0.9.0.0", + "value": "0.9.0.0" + }, + { + "children": [], + "label": "0.8.2.2", + "value": "0.8.2.2" + }, + { + "children": [], + "label": "0.8.2.1", + "value": "0.8.2.1" + }, + { + "children": [], + "label": "0.8.2.0", + "value": "0.8.2.0" + } + ], + "x-index": 1, + "x-designable-id": "uga7qtv47da" + }, + "topic": { + "type": "string", + "title": "Topic", + "x-decorator": "FormItem", + "x-component": "Input", + "x-validator": [], + "x-component-props": {}, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "topic", + "required": true, + "x-designable-id": "27le3kmhbca", + "x-index": 2 + }, + "address": { + "type": "string", + "title": "服务器地址", + "x-decorator": "FormItem", + "x-component": "Input", + "x-validator": [], + "x-component-props": {}, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "address", + "x-index": 3, + "required": true, + "x-designable-id": "gz77bhn4f9d" + }, + "partition_type": { + "title": "Partition Type", + "x-decorator": "FormItem", + "x-component": "Select", + "x-validator": [], + "x-component-props": {}, + "required": true, + "name": "partition_type", + "enum": [ + { + "children": [], + "label": "robin", + "value": "robin" + }, + { + "children": [], + "label": "hash", + "value": "warn" + }, + { + "children": [], + "label": "manual", + "value": "manual" + }, + { + "children": [], + "label": "random", + "value": "random" + } + ], + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "x-index": 4, + "x-designable-id": "u939zlo1suv", + "default": "robin" + }, + "partition": { + "type": "number", + "title": "Partition", + "x-decorator": "FormItem", + "x-component": "NumberPicker", + "x-validator": [], + "x-component-props": {}, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "partition", + "required": false, + "x-index": 5, + "x-reactions": { + "dependencies": [ + "partition_type" + ], + "when": "{{$deps[0] === 'manual'}}", + "fulfill": { + "state": { + "visible": true + } + }, + "otherwise": { + "state": { + "visible": false + } + } + } + }, + "partition_key": { + "type": "string", + "title": "Partition Key", + "x-decorator": "FormItem", + "x-component": "Input", + "x-validator": [], + "x-component-props": {}, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "partition_key", + "required": false, + "x-designable-id": "w3y6n0ali8z", + "x-index": 6, + "x-reactions": { + "dependencies": [ + "partition_type" + ], + "when": "{{$deps[0] === 'hash'}}", + "fulfill": { + "state": { + "visible": true + } + }, + "otherwise": { + "state": { + "visible": false + } + } + } + }, + "timeout": { + "type": "number", + "title": "请求超时时间", + "x-decorator": "FormItem", + "x-component": "NumberPicker", + "x-validator": [ + { + "triggerType": "onInput", + "min": 0 + } + ], + "x-component-props": {}, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "required": true, + "default": 10, + "name": "timeout", + "description": "单位:s,最小值:1", + "x-index": 7 + }, + "type": { + "title": "输出格式", + "x-decorator": "FormItem", + "x-component": "Select", + "x-validator": [], + "x-component-props": {}, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "enum": [ + { + "children": [], + "label": "单行", + "value": "line" + }, + { + "children": [], + "label": "Json", + "value": "json" + } + ], + "default": "line", + "name": "type", + "x-index": 8, + "required": true + }, + "formatter": { + "type": "object", + "title": "格式化配置", + "x-decorator": "FormItem", + "x-component": "CustomCodeboxComponent", + "x-component-props": { + "mode": "json" + }, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "formatter", + "x-index": 9 + } + } + } \ No newline at end of file diff --git a/module/dynamic-module/driver/embed/nsqd-access-log/NSQ日志.png b/module/dynamic-module/driver/embed/nsqd-access-log/NSQ日志.png new file mode 100644 index 00000000..44828dc9 Binary files /dev/null and b/module/dynamic-module/driver/embed/nsqd-access-log/NSQ日志.png differ diff --git a/module/dynamic-module/driver/embed/nsqd-access-log/README.md b/module/dynamic-module/driver/embed/nsqd-access-log/README.md new file mode 100644 index 00000000..6e3b87a6 --- /dev/null +++ b/module/dynamic-module/driver/embed/nsqd-access-log/README.md @@ -0,0 +1,102 @@ +# NSQ日志 + +## 基本介绍 + +日志是用来暴露系统内部状态的一种手段,好的日志可以帮助开发人员快速定位问题所在,然后找到合适的方式解决掉问题。该插件支持将`节点访问日志`输出到**NSQ队列**中。 + +## 功能特性 + +NSQ日志:能够将程序运行中产生的日志内容输出到指定NSQD的topic中。 + +* 支持填写多个nsqd请求地址 +* 支持日志输出格式类型 +* 支持自定义日志格式化配置 + +## 功能演示 + +### 新建NSQ日志配置 + +1、点击左侧导航栏`系统管理` -> `NSQ日志`,进入 `NSQ日志`列表页面,点击`新建NSQ日志` + +![](http://data.eolinker.com/course/mP9cAUw2c0e57b252f4ede635f63b76ba31f5c0b826872a.png) + +2、填写NSQ日志配置 + +![](http://data.eolinker.com/course/jzPEqMc41180eeb8d1a8cb7a9d1ff545753fb712f2df4ac.png) + +**配置说明**: + +| 字段名称 | 说明 | +| :----------- | :----------------------------------------------------------- | +| NSQD地址列表 | NSQD提供TCP服务的地址列表,支持填写多个地址 | +| Topic | NSQD的Topic信息 | +| 鉴权Secret | 配置访问NSQD的鉴权密钥信息 | +| 输出格式 | 输出日志内容格式,支持单行、Json格式输出 | +| 格式化配置 | 输出格式模版,配置教程[点此](https://help.apinto.com/docs/formatter)进行跳转 | + +**示例格式化配置** + +``` +{ + "fields": [ + "$time_iso8601", + "$request_id", + "@request", + "@proxy", + "@response", + "@status_code", + "@time" + ], + "request": [ + "$request_method", + "$scheme", + "$request_uri", + "$host", + "$header", + "$remote_addr" + ], + "proxy": [ + "$proxy_method", + "$proxy_scheme", + "$proxy_uri", + "$proxy_host", + "$proxy_header", + "$proxy_addr" + ], + "response": [ + "$response_header" + ], + "status_code": [ + "$status", + "$proxy_status" + ], + "time": [ + "$request_time", + "$response_time" + ] +} +``` + +3、点击确定后,NSQ日志添加完成 + +![](http://data.eolinker.com/course/9bk8JLP81dd351417cf20d1cf84b8480e27056567f4f7d3.png) + +### 发布到集群 + +1、点击列表右侧`小飞机`按钮,将NSQ日志配置发布上线 + +![](http://data.eolinker.com/course/7QPtDzY71d87af4a504468343eb0c80ccca823c93726a48.png) + +2、选择其中需要发布上线的环境,点击`上线` + +![](http://data.eolinker.com/course/AJdKFlMd13cf76912566ee666427f28e9ecfcf594a70bfc.png) + +3、上线成功后,列表会实时显示相应集群的发布状态 + +![](http://data.eolinker.com/course/UsgdW3t020287005ddd27f229bed6532a17f2c6a8d1e9c7.png) + +## 更新日志 + +### V1.0(2023-6-19) + +- 插件上线 \ No newline at end of file diff --git a/module/dynamic-module/driver/embed/nsqd-access-log/plugin.yml b/module/dynamic-module/driver/embed/nsqd-access-log/plugin.yml new file mode 100644 index 00000000..fd79cda7 --- /dev/null +++ b/module/dynamic-module/driver/embed/nsqd-access-log/plugin.yml @@ -0,0 +1,217 @@ +id: "nsqd-access-log.apinto.com" +name: "nsqd-access-log" +cname: "NSQ日志" +resume: "将请求和响应日志发送到NSQ中" +version: "v1.0.0" +icon: "NSQ日志.png" +driver: "dynamic.apinto.com" +front: template/nsqd-access-log +navigation: "navigation.system" +group_id: "log" +frontend: + - name: nsqd-access-log + driver: apinto.intelligent.normal + router: + - path: template/nsqd-access-log + type: normal +define: # 动态模块定义 + profession: output + drivers: + - name: nsqd + title: NSQD + skill: Access-Output + fields: + - name: title # 定义从响应中对应字段中获取显示值 + title: 名称 + - name: id + title: ID + - name: driver + title: 驱动名称 + - name: description + title: 描述 + render: + nsqd: | + { + "type": "object", + "properties": { + "scopes": { + "type": "array", + "title": "作用范围", + "x-decorator": "FormItem", + "x-component": "ArrayItems", + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "scopes", + "x-index": 0, + "required": true, + "items": { + "type": "void", + "x-component": "Space", + "properties": { + "sort": { + "type": "void", + "x-decorator": "FormItem", + "x-component": "ArrayItems.SortHandle", + "name": "sort", + "x-index": 0 + }, + "select": { + "type": "string", + "x-decorator": "FormItem", + "x-component": "Select", + "enum": [ + { + "label": "Access日志", + "value": "access_log" + } + ], + "name": "select", + "x-index": 1 + }, + "remove": { + "type": "void", + "x-decorator": "FormItem", + "x-component": "ArrayItems.Remove", + "name": "remove", + "x-index": 2 + } + } + }, + "properties": { + "add": { + "type": "void", + "title": "添加条目", + "x-component": "ArrayItems.Addition", + "x-component-props": { + "defaultValue": "access_log" + }, + "name": "add", + "x-index": 0 + } + } + }, + "address": { + "type": "array", + "title": "NSQD地址列表", + "x-decorator": "FormItem", + "x-component": "ArrayItems", + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "address", + "x-index": 1, + "required": true, + "items": { + "type": "void", + "x-component": "Space", + "properties": { + "sort": { + "type": "void", + "x-decorator": "FormItem", + "x-component": "ArrayItems.SortHandle", + "name": "sort", + "x-index": 0 + }, + "input": { + "type": "string", + "x-decorator": "FormItem", + "x-component": "Input", + "name": "input", + "x-index": 1 + }, + "remove": { + "type": "void", + "x-decorator": "FormItem", + "x-component": "ArrayItems.Remove", + "name": "remove", + "x-index": 2 + } + } + }, + "properties": { + "add": { + "type": "void", + "title": "添加地址", + "x-component": "ArrayItems.Addition", + "name": "add", + "x-index": 0 + } + } + }, + "topic": { + "type": "string", + "title": "Topic", + "x-decorator": "FormItem", + "x-component": "Input", + "x-validator": [], + "x-component-props": {}, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "topic", + "x-index": 2, + "required": true + }, + "auth_secret": { + "type": "string", + "title": "鉴权Secret", + "x-decorator": "FormItem", + "x-component": "Input", + "x-validator": "url", + "x-component-props": {}, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "auth_secret", + "x-index": 3, + "required": false + }, + "type": { + "title": "输出格式", + "x-decorator": "FormItem", + "x-component": "Select", + "x-validator": [], + "x-component-props": {}, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "enum": [ + { + "children": [], + "label": "单行", + "value": "line" + }, + { + "children": [], + "label": "Json", + "value": "json" + } + ], + "default": "line", + "name": "type", + "x-index": 4, + "required": true + }, + "formatter": { + "type": "object", + "title": "格式化配置", + "x-decorator": "FormItem", + "x-component": "CustomCodeboxComponent", + "x-component-props": { + "mode": "json" + }, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "formatter", + "x-index": 5 + } + } + } \ No newline at end of file diff --git a/module/dynamic-module/driver/embed/redis/README.md b/module/dynamic-module/driver/embed/redis/README.md new file mode 100644 index 00000000..8514637d --- /dev/null +++ b/module/dynamic-module/driver/embed/redis/README.md @@ -0,0 +1,8 @@ +## 基本介绍 +Redis是一个开源(BSD许可)的内存数据结构存储,用作数据库、缓存、消息代理和流媒体引擎。Redis提供数据结构,如字符串、散列、列表、集合、带范围查询的排序集合、位图、超日志、地理空间索引和流。Redis具有内置复制、Lua脚本、LRU驱逐、事务和不同级别的磁盘持久性,并通过Redis Sentinel和Redis Cluster的自动分区提供高可用性。该插件支持配置Redis Cluster信息,从而使Apinto节点接入Redis。 +## 功能特性 +- 配置Redis Cluster信息,帮助Apinto节点接入Redis。 +- Apinto多个策略、插件依赖Redis,配置Redis后,使用Redis作为缓存数据库。 +## 更新日志 +### V1.0(2023-4-30) +- 插件上线 \ No newline at end of file diff --git a/module/dynamic-module/driver/embed/redis/Redis配置.png b/module/dynamic-module/driver/embed/redis/Redis配置.png new file mode 100644 index 00000000..0476c91d Binary files /dev/null and b/module/dynamic-module/driver/embed/redis/Redis配置.png differ diff --git a/module/dynamic-module/driver/embed/redis/plugin.yml b/module/dynamic-module/driver/embed/redis/plugin.yml new file mode 100644 index 00000000..1cfee933 --- /dev/null +++ b/module/dynamic-module/driver/embed/redis/plugin.yml @@ -0,0 +1,120 @@ +id: "redis.apinto.com" +name: "redis" +cname: "Redis配置" +resume: "配置Redis Cluster信息,帮助Apinto节点接入Redis资源。" +version: "v1.0.0" +icon: "Redis配置.png" +driver: "dynamic.apinto.com" +front: template/redis +navigation: "navigation.system" +group_id: "resource" +frontend: + - name: redis + driver: apinto.intelligent.normal + router: + - path: template/redis + type: normal +define: # 动态模块定义 + profession: output + drivers: + - name: redis + title: Redis + skill: Cache + fields: + - name: title # 定义从响应中对应字段中获取显示值 + title: 名称 + - name: id + title: ID + - name: driver + title: 驱动名称 + - name: description + title: 描述 + render: + redis: | + { + "type":"object", + "properties":{ + "addrs":{ + "type":"array", + "title":"Redis节点列表", + "x-decorator":"FormItem", + "x-component":"ArrayItems", + "x-decorator-props":{ + "labelCol":6, + "wrapperCol":10 + }, + "items":{ + "type":"void", + "x-component":"Space", + "properties":{ + "sort":{ + "type":"void", + "x-decorator":"FormItem", + "x-component":"ArrayItems.SortHandle", + "x-index":0 + }, + "select":{ + "type":"string", + "x-decorator":"FormItem", + "x-component":"Input", + "x-index":1 + }, + "remove":{ + "type":"void", + "x-decorator":"FormItem", + "x-component":"ArrayItems.Remove", + "x-index":2 + } + } + }, + "properties":{ + "add":{ + "type":"void", + "title":"添加节点", + "x-component":"ArrayItems.Addition" + } + }, + "name":"addrs", + "x-index":0, + "required":true + }, + "username":{ + "type":"string", + "title":"用户名", + "x-decorator":"FormItem", + "x-component":"Input", + "x-validator":[ + + ], + "x-component-props":{ + + }, + "x-decorator-props":{ + "labelCol":6, + "wrapperCol":10 + }, + "name":"username", + "x-index":1, + "required":false + }, + "password":{ + "type":"string", + "title":"密码", + "x-decorator":"FormItem", + "x-component":"Password", + "x-component-props":{ + "checkStrength":true + }, + "x-validator":[ + + ], + "x-decorator-props":{ + "labelCol":6, + "wrapperCol":10 + }, + "name":"password", + "x-index":2, + "required":false + } + } + } \ No newline at end of file diff --git a/module/dynamic-module/driver/embed/syslog-access-log/README.md b/module/dynamic-module/driver/embed/syslog-access-log/README.md new file mode 100644 index 00000000..fcfaf6f4 --- /dev/null +++ b/module/dynamic-module/driver/embed/syslog-access-log/README.md @@ -0,0 +1,104 @@ +# Syslog日志 + +## 基本介绍 + +日志是用来暴露系统内部状态的一种手段,好的日志可以帮助开发人员快速定位问题所在,然后找到合适的方式解决掉问题。该插件支持将`节点访问日志`输出到`Syslog`中。 + +## 功能特性 + +Syslog日志:能够将程序运行中产生的日志内容输出远端的Syslog服务器。 + +* 支持多种请求协议,包括TCP、UDP、UNIX +* 支持设置Syslog输出日志等级 +* 支持日志输出格式类型 +* 支持自定义日志格式化配置 + +## 功能演示 + +### 新建Syslog日志配置 + +1、点击左侧导航栏`系统管理` -> `Syslog日志`,进入 `Syslog日志`列表页面,点击`新建Syslog日志` + +![](http://data.eolinker.com/course/JjBrSvS208f58f53d5792de7ca068c9674b97ffdaa4e3a7.png) + +2、填写Syslog日志配置 + +![](http://data.eolinker.com/course/c456gUa9c273f26aef61fc77d0a7e7cb3513b9430b1f7f8.png) + +**配置说明**: + + +| 字段名称 | 说明 | +| :--------- | :----------------------------------------------------------- | +| 网络协议 | 请求Syslog服务的协议,支持TCP、UDP、UNIX | +| 服务器地址 | Syslog服务的地址 | +| 日志等级 | Syslog输出日志等级,支持ERROR、WARN、INFO、DEBUG、TRACE | +| 输出格式 | 输出日志内容格式,支持单行、Json格式输出 | +| 格式化配置 | 输出格式模版,配置教程[点此](https://help.apinto.com/docs/formatter)进行跳转 | + +**示例格式化配置** + +``` +{ + "fields": [ + "$time_iso8601", + "$request_id", + "@request", + "@proxy", + "@response", + "@status_code", + "@time" + ], + "request": [ + "$request_method", + "$scheme", + "$request_uri", + "$host", + "$header", + "$remote_addr" + ], + "proxy": [ + "$proxy_method", + "$proxy_scheme", + "$proxy_uri", + "$proxy_host", + "$proxy_header", + "$proxy_addr" + ], + "response": [ + "$response_header" + ], + "status_code": [ + "$status", + "$proxy_status" + ], + "time": [ + "$request_time", + "$response_time" + ] +} +``` + +3、点击确定后,Syslog日志添加完成 + +![](http://data.eolinker.com/course/igk2H3K0bfe9981f3213795e585b167da62ff91cf3d46f2.png) + +### 发布到集群 + +1、点击列表右侧`小飞机`按钮,将Syslog日志配置发布上线 + +![](http://data.eolinker.com/course/SlvdD1aecbd3d4c58a5b9ec073ad15cffe23caf199d398f.png) + +2、选择其中需要发布上线的环境,点击`上线` + +![](http://data.eolinker.com/course/LzlDAsN472f7c71489d56f9b3a20fd0c35ff19223d7cf50.png) + +3、上线成功后,列表会实时显示相应集群的发布状态 + +![](http://data.eolinker.com/course/NTSZ6zHc0eb936c502e19e13edb9faefb3886010e432d3d.png) + +## 更新日志 + +### V1.0(2023-6-19) + +- 插件上线 \ No newline at end of file diff --git a/module/dynamic-module/driver/embed/syslog-access-log/SYSLOG日志.png b/module/dynamic-module/driver/embed/syslog-access-log/SYSLOG日志.png new file mode 100644 index 00000000..c62403ae Binary files /dev/null and b/module/dynamic-module/driver/embed/syslog-access-log/SYSLOG日志.png differ diff --git a/module/dynamic-module/driver/embed/syslog-access-log/plugin.yml b/module/dynamic-module/driver/embed/syslog-access-log/plugin.yml new file mode 100644 index 00000000..8f75dc78 --- /dev/null +++ b/module/dynamic-module/driver/embed/syslog-access-log/plugin.yml @@ -0,0 +1,225 @@ +id: "syslog-access-log.apinto.com" +name: "syslog-access-log" +cname: "Syslog日志" +resume: "将请求和响应日志发送到Syslog中" +version: "v1.0.0" +icon: "SYSLOG日志.png" +driver: "dynamic.apinto.com" +front: template/syslog-access-log +navigation: "navigation.system" +group_id: "log" +frontend: + - name: syslog-access-log + driver: apinto.intelligent.normal + router: + - path: template/syslog-access-log + type: normal +define: # 动态模块定义 + profession: output + drivers: + - name: syslog_output + title: Syslog + skill: Access-Output + fields: + - name: title # 定义从响应中对应字段中获取显示值 + title: 名称 + - name: id + title: ID + - name: driver + title: 驱动名称 + - name: description + title: 描述 + render: + syslog_output: | + { + "type": "object", + "properties": { + "scopes": { + "type": "array", + "title": "作用范围", + "x-decorator": "FormItem", + "x-component": "ArrayItems", + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "scopes", + "x-index": 0, + "required": true, + "items": { + "type": "void", + "x-component": "Space", + "properties": { + "sort": { + "type": "void", + "x-decorator": "FormItem", + "x-component": "ArrayItems.SortHandle", + "name": "sort", + "x-index": 0 + }, + "select": { + "type": "string", + "x-decorator": "FormItem", + "x-component": "Select", + "enum": [ + { + "label": "Access日志", + "value": "access_log" + } + ], + "name": "select", + "x-index": 1 + }, + "remove": { + "type": "void", + "x-decorator": "FormItem", + "x-component": "ArrayItems.Remove", + "name": "remove", + "x-index": 2 + } + } + }, + "properties": { + "add": { + "type": "void", + "title": "添加条目", + "x-component": "ArrayItems.Addition", + "x-component-props": { + "defaultValue": "access_log" + }, + "name": "add", + "x-index": 0 + } + } + }, + "network": { + "title": "网络协议", + "x-decorator": "FormItem", + "x-component": "Select", + "x-validator": [], + "x-component-props": {}, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "network", + "required": true, + "enum": [ + { + "children": [], + "label": "TCP", + "value": "tcp" + }, + { + "children": [], + "label": "UDP", + "value": "udp" + }, + { + "children": [], + "label": "UNIX", + "value": "unix" + } + ], + "x-index": 1 + }, + "address": { + "type": "string", + "title": "服务器地址", + "x-decorator": "FormItem", + "x-component": "Input", + "x-validator": [], + "x-component-props": {}, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "address", + "x-index": 2, + "required": true + }, + "level": { + "title": "日志等级", + "x-decorator": "FormItem", + "x-component": "Select", + "x-validator": [], + "x-component-props": {}, + "required": true, + "name": "level", + "enum": [ + { + "children": [], + "label": "ERROR", + "value": "error" + }, + { + "children": [], + "label": "WARN", + "value": "warn" + }, + { + "children": [], + "label": "INFO", + "value": "info" + }, + { + "children": [], + "label": "DEBUG", + "value": "debug" + }, + { + "children": [], + "label": "TRACE", + "value": "trace" + } + ], + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "x-index": 3 + }, + "type": { + "title": "输出格式", + "x-decorator": "FormItem", + "x-component": "Select", + "x-validator": [], + "x-component-props": {}, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "enum": [ + { + "children": [], + "label": "单行", + "value": "line" + }, + { + "children": [], + "label": "Json", + "value": "json" + } + ], + "default": "line", + "name": "type", + "x-index": 4, + "required": true + }, + "formatter": { + "type": "object", + "title": "格式化配置", + "x-decorator": "FormItem", + "x-component": "CustomCodeboxComponent", + "x-component-props": { + "mode": "json" + }, + "x-decorator-props": { + "labelCol": 6, + "wrapperCol": 10 + }, + "name": "formatter", + "x-index": 5 + } + } + } \ No newline at end of file diff --git a/module/dynamic-module/driver/install.go b/module/dynamic-module/driver/install.go new file mode 100644 index 00000000..bce7c468 --- /dev/null +++ b/module/dynamic-module/driver/install.go @@ -0,0 +1 @@ +package driver diff --git a/module/dynamic-module/driver/load.go b/module/dynamic-module/driver/load.go new file mode 100644 index 00000000..337041ce --- /dev/null +++ b/module/dynamic-module/driver/load.go @@ -0,0 +1,47 @@ +package driver + +import ( + "embed" + "path" + + "github.com/eolinker/eosc/log" +) + +var ( + //go:embed embed + pluginDir embed.FS +) + +func init() { + err := LoadPlugins(&pluginDir, "embed", "plugin.yml") + if err != nil { + panic(err) + } +} + +func LoadPlugins(fs *embed.FS, dir string, target string) error { + entries, err := fs.ReadDir(dir) + if err != nil { + return err + } + + for _, e := range entries { + if !e.IsDir() { + continue + } + filePath := path.Join(dir, e.Name(), target) + fileContent, err := fs.ReadFile(filePath) + if err != nil { + return err + } + pluginCfg, err := Read(fileContent) + if err != nil { + log.Errorf("read inert plugin file(%s) error: %v", filePath, err) + return err + } + Register(NewDriver(pluginCfg)) + + } + + return err +} diff --git a/module/dynamic-module/driver/manager.go b/module/dynamic-module/driver/manager.go new file mode 100644 index 00000000..fc386ee3 --- /dev/null +++ b/module/dynamic-module/driver/manager.go @@ -0,0 +1,80 @@ +package driver + +import "github.com/eolinker/eosc" + +var ( + manager = NewManager() +) + +func NewManager() *Manager { + return &Manager{ + drivers: eosc.BuildUntyped[string, IDriver](), + driversByGroup: eosc.BuildUntyped[string, DynamicDrivers](), + } +} + +type DynamicDrivers eosc.Untyped[string, IDriver] + +type Manager struct { + drivers DynamicDrivers + driversByGroup eosc.Untyped[string, DynamicDrivers] +} + +func (m *Manager) Register(driver IDriver) { + m.drivers.Set(driver.Name(), driver) + if drivers, ok := m.driversByGroup.Get(driver.Group()); ok { + drivers.Set(driver.Name(), driver) + } else { + drivers = eosc.BuildUntyped[string, IDriver]() + drivers.Set(driver.Name(), driver) + m.driversByGroup.Set(driver.Group(), drivers) + } +} + +func (m *Manager) Get(name string) (IDriver, bool) { + return m.drivers.Get(name) +} + +func (m *Manager) List() []IDriver { + return m.drivers.List() +} + +func (m *Manager) ListByGroup(group string) []IDriver { + if drivers, ok := m.driversByGroup.Get(group); ok { + return drivers.List() + } + return nil +} + +func (m *Manager) Drivers() []string { + return m.drivers.Keys() +} + +func (m *Manager) DriversByGroup(group string) []string { + if drivers, ok := m.driversByGroup.Get(group); ok { + return drivers.Keys() + } + return nil +} + +func Register(driver IDriver) { + manager.Register(driver) +} + +func Get(name string) (IDriver, bool) { + return manager.Get(name) +} + +func Drivers(group ...string) []string { + if len(group) > 0 && group[0] != "" { + return manager.DriversByGroup(group[0]) + } + return manager.Drivers() +} + +func List(group ...string) []IDriver { + if len(group) > 0 && group[0] != "" { + return manager.ListByGroup(group[0]) + } + return manager.List() +} diff --git a/module/dynamic-module/driver/plugin.go b/module/dynamic-module/driver/plugin.go new file mode 100644 index 00000000..ee239533 --- /dev/null +++ b/module/dynamic-module/driver/plugin.go @@ -0,0 +1,29 @@ +package driver + +type PluginCfg struct { + Id string `json:"id,omitempty" yaml:"id"` + Name string `json:"name,omitempty" yaml:"name"` + Cname string `json:"cname,omitempty" yaml:"cname"` + Resume string `json:"resume,omitempty" yaml:"resume"` + Version string `json:"version,omitempty" yaml:"version"` + ICon string `json:"icon,omitempty" yaml:"icon"` + Driver string `json:"driver,omitempty" yaml:"driver"` + GroupId string `json:"group_id,omitempty" yaml:"group_id"` + Front string `json:"front,omitempty" yaml:"front"` + Define *PluginDefine `json:"define,omitempty" yaml:"define"` +} + +type PluginDefine struct { + Profession string `yaml:"profession"` + Drivers []*Field `yaml:"drivers"` + Skill string `yaml:"skill"` + Fields []*Field `yaml:"fields"` + Render map[string]string `yaml:"render"` +} + +type Field struct { + Name string `yaml:"name"` + Title string `yaml:"title"` + Attr string `json:"attr,omitempty"` + Enum []string `json:"enum,omitempty"` +} diff --git a/module/dynamic-module/driver/read.go b/module/dynamic-module/driver/read.go new file mode 100644 index 00000000..898058a5 --- /dev/null +++ b/module/dynamic-module/driver/read.go @@ -0,0 +1,24 @@ +package driver + +import ( + "gopkg.in/yaml.v3" +) + +func Read(input []byte) (*PluginCfg, error) { + + p := new(PluginCfg) + + err := yaml.Unmarshal(input, &p) + if err != nil { + return nil, err + } + err = yaml.Unmarshal(input, p) + if err != nil { + return nil, err + } + if p.Version == "" { + p.Version = "v0.0.0" + } + + return p, nil +} diff --git a/module/dynamic-module/dto/input.go b/module/dynamic-module/dto/input.go new file mode 100644 index 00000000..241aeadf --- /dev/null +++ b/module/dynamic-module/dto/input.go @@ -0,0 +1,21 @@ +package dynamic_module_dto + +type CreateDynamicModule struct { + Id string `json:"id"` + Name string `json:"title"` + Driver string `json:"driver"` + Description string `json:"description"` + Config map[string]interface{} `json:"config"` +} + +//type PartitionConfig map[string]interface{} + +type EditDynamicModule struct { + Name *string `json:"title"` + Description *string `json:"description"` + Config *map[string]interface{} `json:"config"` +} + +type ClusterInput struct { + Clusters []string `json:"clusters" aocheck:"cluster"` +} diff --git a/module/dynamic-module/dto/output.go b/module/dynamic-module/dto/output.go new file mode 100644 index 00000000..fe65ddba --- /dev/null +++ b/module/dynamic-module/dto/output.go @@ -0,0 +1,54 @@ +package dynamic_module_dto + +import ( + "github.com/eolinker/go-common/auto" +) + +type DynamicModule struct { + Id string `json:"id"` + Name string `json:"title"` + Driver string `json:"driver"` + Description string `json:"description"` + Config map[string]interface{} `json:"config"` +} + +type PluginBasic struct { + Id string `json:"id"` + Name string `json:"name"` + Title string `json:"title"` +} + +type PluginInfo struct { + *PluginBasic + Drivers []*Field `json:"drivers"` + Fields []*Field `json:"fields"` +} + +type Field struct { + Name string `json:"name"` + Title string `json:"title"` + Attr string `json:"attr,omitempty"` + Enum []string `json:"enum,omitempty"` +} + +type ModuleDriver struct { + Name string `json:"name"` + Title string `json:"title"` + Path string `json:"path"` +} + +type OnlineInfo struct { + Id string `json:"id"` + Name string `json:"name"` + Title string `json:"title"` + Description string `json:"description"` + Partitions []*PartitionInfo `json:"partitions"` +} + +type PartitionInfo struct { + Name string `json:"name"` + Title string `json:"title"` + Status string `json:"status"` + Updater auto.Label `json:"updater" aolabel:"user"` + UpdateTime auto.TimeLabel `json:"update_time"` +} diff --git a/module/dynamic-module/dynamic_module.go b/module/dynamic-module/dynamic_module.go new file mode 100644 index 00000000..8e678d79 --- /dev/null +++ b/module/dynamic-module/dynamic_module.go @@ -0,0 +1,36 @@ +package dynamic_module + +import ( + "context" + "reflect" + + "github.com/APIParkLab/APIPark/gateway" + + "github.com/eolinker/go-common/autowire" + + dynamic_module_dto "github.com/APIParkLab/APIPark/module/dynamic-module/dto" +) + +type IDynamicModuleModule interface { + Create(ctx context.Context, module string, input *dynamic_module_dto.CreateDynamicModule) (*dynamic_module_dto.DynamicModule, error) + Edit(ctx context.Context, module string, id string, input *dynamic_module_dto.EditDynamicModule) (*dynamic_module_dto.DynamicModule, error) + Delete(ctx context.Context, module string, ids []string) error + Get(ctx context.Context, module string, id string) (*dynamic_module_dto.DynamicModule, error) + List(ctx context.Context, module string, keyword string, page int, pageSize int) ([]map[string]interface{}, int64, error) + PluginInfo(ctx context.Context, module string, clusterIds ...string) (*dynamic_module_dto.PluginInfo, error) + Render(ctx context.Context, module string) (map[string]interface{}, error) + ModuleDrivers(ctx context.Context, group string) ([]*dynamic_module_dto.ModuleDriver, error) + + Online(ctx context.Context, module string, id string, clusterInput *dynamic_module_dto.ClusterInput) error + Offline(ctx context.Context, module string, id string, clusterInput *dynamic_module_dto.ClusterInput) error + //PartitionStatuses(ctx context.Context, module string, keyword string, page int, pageSize int) (map[string]map[string]string, error) + //PartitionStatus(ctx context.Context, module string, id string) (*dynamic_module_dto.OnlineInfo, error) +} + +func init() { + autowire.Auto[IDynamicModuleModule](func() reflect.Value { + m := new(imlDynamicModule) + gateway.RegisterInitHandleFunc(m.initGateway) + return reflect.ValueOf(m) + }) +} diff --git a/module/dynamic-module/iml.go b/module/dynamic-module/iml.go new file mode 100644 index 00000000..02e696e1 --- /dev/null +++ b/module/dynamic-module/iml.go @@ -0,0 +1,530 @@ +package dynamic_module + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/eolinker/eosc/log" + + "github.com/APIParkLab/APIPark/gateway" + + "github.com/eolinker/go-common/store" + + "github.com/eolinker/ap-account/service/user" + + "github.com/APIParkLab/APIPark/service/cluster" + + "github.com/eolinker/go-common/utils" + + "github.com/google/uuid" + + "github.com/APIParkLab/APIPark/module/dynamic-module/driver" + + dynamic_module_dto "github.com/APIParkLab/APIPark/module/dynamic-module/dto" + dynamic_module "github.com/APIParkLab/APIPark/service/dynamic-module" +) + +var _ IDynamicModuleModule = (*imlDynamicModule)(nil) + +type imlDynamicModule struct { + clusterService cluster.IClusterService `autowired:""` + dynamicModuleService dynamic_module.IDynamicModuleService `autowired:""` + dynamicModulePublishService dynamic_module.IDynamicModulePublishService `autowired:""` + userService user.IUserService `autowired:""` + transaction store.ITransaction `autowired:""` +} + +func (i *imlDynamicModule) initGateway(ctx context.Context, clusterId string, clientDriver gateway.IClientDriver) error { + // TODO: 初始化集群操作 + return nil +} + +func (i *imlDynamicModule) Online(ctx context.Context, module string, id string, clusterInput *dynamic_module_dto.ClusterInput) error { + _, has := driver.Get(module) + if !has { + return fmt.Errorf("模块【%s】不存在", module) + } + //if len(clusterInput.Clusters) == 0 { + // return fmt.Errorf("上线分区失败,分区为空") + //} + + id = strings.ToLower(fmt.Sprintf("%s_%s", id, module)) + info, err := i.dynamicModuleService.Get(ctx, id) + if err != nil { + return fmt.Errorf("上线失败,配置不存在") + } + clusters, err := i.clusterService.List(ctx, clusterInput.Clusters...) + if err != nil || len(clusters) == 0 { + return fmt.Errorf("上线失败,集群不存在") + } + + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + for _, c := range clusters { + + // 插入发布历史 + err = i.dynamicModulePublishService.Create(ctx, &dynamic_module.CreateDynamicModulePublish{ + ID: uuid.New().String(), + DynamicModule: id, + Module: module, + Cluster: c.Uuid, + Version: info.Version, + }) + if err != nil { + return err + } + err = i.dynamicClient(ctx, c.Uuid, module, func(dynamicClient gateway.IDynamicClient) error { + cfg := &gateway.DynamicRelease{} + err = json.Unmarshal([]byte(info.Config), &cfg) + if err != nil { + return err + } + cfg.ID = id + cfg.Version = info.Version + cfg.MatchLabels = map[string]string{ + "module": module, + } + err = dynamicClient.Online(ctx, cfg) + if err != nil { + return err + } + return nil + }) + if err != nil { + return err + } + + } + + return nil + }) +} + +func (i *imlDynamicModule) Offline(ctx context.Context, module string, id string, clusterInput *dynamic_module_dto.ClusterInput) error { + _, has := driver.Get(module) + if !has { + return fmt.Errorf("模块【%s】不存在", module) + } + //if len(clusterInput.Clusters) == 0 { + // return fmt.Errorf("下线分区失败,分区为空") + //} + + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + id = strings.ToLower(fmt.Sprintf("%s_%s", id, module)) + if len(clusterInput.Clusters) == 0 { + clusters, err := i.clusterService.List(ctx) + if err != nil { + return err + } + clusterInput.Clusters = make([]string, 0) + for _, c := range clusters { + clusterInput.Clusters = append(clusterInput.Clusters, c.Uuid) + } + } + for _, clusterId := range clusterInput.Clusters { + err := i.dynamicClient(ctx, clusterId, module, func(dynamicClient gateway.IDynamicClient) error { + return dynamicClient.Offline(ctx, &gateway.DynamicRelease{ + BasicItem: &gateway.BasicItem{ + ID: id, + }, + }) + }) + if err != nil { + return err + } + + } + return nil + }) +} + +// +//func (i *imlDynamicModule) PartitionStatuses(ctx context.Context, module string, keyword string, page int, pageSize int) (map[string]map[string]string, error) { +// _, has := driver.Get(module) +// if !has { +// return nil, fmt.Errorf("模块【%s】不存在", module) +// } +// list, _, err := i.dynamicModuleService.SearchByPage(ctx, keyword, map[string]interface{}{ +// "module": module, +// }, page, pageSize, "update_at desc") +// if err != nil { +// return nil, err +// } +// partitions, err := i.partitionService.List(ctx) +// if err != nil { +// return nil, err +// } +// out := make(map[string]map[string]string) +// for _, c := range partitions { +// err := i.dynamicClient(ctx, c.Cluster, module, func(dynamicClient gateway.IDynamicClient) error { +// versions, err := dynamicClient.Versions(ctx, map[string]string{ +// "module": module, +// }) +// if err != nil { +// return err +// } +// for _, l := range list { +// id := strings.TrimSuffix(l.ID, fmt.Sprintf("_%s", module)) +// if _, ok := out[id]; !ok { +// out[id] = make(map[string]string) +// } +// +// out[id][c.UUID] = "未发布" +// if v, ok := versions[strings.ToLower(l.ID)]; ok { +// if v == l.Version { +// out[id][c.UUID] = "已发布" +// } else { +// out[id][c.UUID] = "待发布" +// } +// } +// } +// return nil +// }) +// if err != nil { +// return nil, err +// } +// +// } +// return out, nil +//} + +func (i *imlDynamicModule) dynamicClient(ctx context.Context, clusterId string, resource string, h func(gateway.IDynamicClient) error) error { + client, err := i.clusterService.GatewayClient(ctx, clusterId) + + if err != nil { + return err + } + defer func() { + err := client.Close(ctx) + if err != nil { + log.Warn("close apinto client:", err) + } + }() + dynamic, err := client.Dynamic(resource) + if err != nil { + return err + } + return h(dynamic) +} + +// +//func (i *imlDynamicModule) PartitionStatus(ctx context.Context, module string, id string) (*dynamic_module_dto.OnlineInfo, error) { +// _, has := driver.Get(module) +// +// if !has { +// return nil, fmt.Errorf("模块【%s】不存在", module) +// } +// partitions, err := i.partitionService.List(ctx) +// if err != nil { +// return nil, err +// } +// partitionIds := utils.SliceToSlice(partitions, func(s *partition.Partition) string { +// return s.UUID +// }) +// suffix := fmt.Sprintf("_%s", module) +// id = id + suffix +// info, err := i.dynamicModuleService.Get(ctx, id) +// if err != nil { +// return nil, err +// } +// publishMap, err := i.dynamicModulePublishService.Latest(ctx, id, partitionIds) +// if err != nil { +// return nil, err +// } +// +// partitionInfos := make([]*dynamic_module_dto.PartitionInfo, 0, len(partitionIds)) +// for _, c := range partitions { +// err := i.dynamicClient(ctx, c.Cluster, module, func(dynamicClient gateway.IDynamicClient) error { +// version, err := dynamicClient.Version(ctx, id) +// if err != nil { +// return err +// } +// updater := "" +// updateTime := time.Time{} +// publishInfo, ok := publishMap[c.UUID] +// if ok { +// updater = publishInfo.Creator +// updateTime = publishInfo.CreateAt +// } +// cInfo := &dynamic_module_dto.PartitionInfo{ +// Name: c.UUID, +// Title: c.Name, +// Status: "未发布", +// Updater: auto.UUID(updater), +// UpdateTime: auto.TimeLabel(updateTime), +// } +// if version == info.Version { +// cInfo.Status = "已发布" +// } else if version != "" { +// cInfo.Status = "待发布" +// } +// partitionInfos = append(partitionInfos, cInfo) +// return nil +// }) +// if err != nil { +// return nil, err +// } +// +// } +// return &dynamic_module_dto.OnlineInfo{ +// Id: strings.TrimSuffix(info.ID, suffix), +// Name: strings.TrimSuffix(info.ID, suffix), +// Title: info.Name, +// Description: info.Description, +// Clusters: partitionInfos, +// }, nil +//} + +func (i *imlDynamicModule) ModuleDrivers(ctx context.Context, group string) ([]*dynamic_module_dto.ModuleDriver, error) { + ds := driver.List(group) + return utils.SliceToSlice(ds, func(s driver.IDriver) *dynamic_module_dto.ModuleDriver { + return &dynamic_module_dto.ModuleDriver{ + Name: s.Name(), + Title: s.Title(), + Path: s.Front(), + } + + }), nil +} + +func (i *imlDynamicModule) Render(ctx context.Context, module string) (map[string]interface{}, error) { + d, has := driver.Get(module) + if !has { + return nil, fmt.Errorf("module %s not found", module) + } + return d.Define().Render(), nil +} + +func (i *imlDynamicModule) PluginInfo(ctx context.Context, module string, clusterIds ...string) (*dynamic_module_dto.PluginInfo, error) { + d, has := driver.Get(module) + if !has { + return nil, fmt.Errorf("module %s not found", module) + } + + fields := make([]*driver.Field, 0, 1) + + fields = append(fields, &driver.Field{ + Name: "status", + Title: fmt.Sprintf("状态"), + Attr: "status", + Enum: []string{ + "已发布", + "待发布", + "未发布", + }, + }) + return &dynamic_module_dto.PluginInfo{ + PluginBasic: &dynamic_module_dto.PluginBasic{ + Id: d.ID(), + Name: d.Name(), + Title: d.Title(), + }, + Drivers: utils.SliceToSlice(d.Define().Drivers(), func(s *driver.Field) *dynamic_module_dto.Field { + return &dynamic_module_dto.Field{ + Name: s.Name, + Title: s.Title, + } + }), + Fields: utils.SliceToSlice(d.Define().Fields(fields...), func(s *driver.Field) *dynamic_module_dto.Field { + return &dynamic_module_dto.Field{ + Name: s.Name, + Title: s.Title, + Attr: s.Attr, + Enum: s.Enum, + } + }), + }, nil +} + +func (i *imlDynamicModule) Create(ctx context.Context, module string, input *dynamic_module_dto.CreateDynamicModule) (*dynamic_module_dto.DynamicModule, error) { + d, has := driver.Get(module) + if !has { + return nil, fmt.Errorf("module %s not found", module) + } + + id := strings.ToLower(fmt.Sprintf("%s_%s", input.Id, module)) + err := i.transaction.Transaction(ctx, func(ctx context.Context) error { + cfg, err := json.Marshal(input.Config) + if err != nil { + return err + } + return i.dynamicModuleService.Create(ctx, &dynamic_module.CreateDynamicModule{ + Id: id, + Name: input.Name, + Driver: input.Driver, + Description: input.Description, + Config: string(cfg), + Module: module, + Profession: d.Define().Profession(), + Skill: d.Define().Skill(), + Version: time.Now().Format("20060102150405"), + }) + }) + if err != nil { + return nil, err + } + + return i.Get(ctx, module, input.Id) +} + +func (i *imlDynamicModule) Edit(ctx context.Context, module string, id string, input *dynamic_module_dto.EditDynamicModule) (*dynamic_module_dto.DynamicModule, error) { + id = strings.ToLower(fmt.Sprintf("%s_%s", id, module)) + _, err := i.get(ctx, module, id) + if err != nil { + return nil, err + } + var cfg *string + var version *string + if input.Config != nil { + tmp, _ := json.Marshal(input.Config) + t := string(tmp) + cfg = &t + v := time.Now().Format("20060102150405") + version = &v + } + err = i.dynamicModuleService.Save(ctx, id, &dynamic_module.EditDynamicModule{ + Name: input.Name, + Description: input.Description, + Config: cfg, + Version: version, + }) + if err != nil { + return nil, err + } + return i.Get(ctx, module, id) +} + +func (i *imlDynamicModule) Delete(ctx context.Context, module string, ids []string) error { + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + for _, id := range ids { + id = strings.ToLower(fmt.Sprintf("%s_%s", id, module)) + _, err := i.get(ctx, module, id) + if err != nil { + return err + } + err = i.dynamicModuleService.Delete(ctx, id) + if err != nil { + return err + } + } + + return nil + }) + +} + +func (i *imlDynamicModule) Get(ctx context.Context, module string, id string) (*dynamic_module_dto.DynamicModule, error) { + suffix := fmt.Sprintf("_%s", module) + if !strings.HasSuffix(id, suffix) { + id = strings.ToLower(fmt.Sprintf("%s_%s", id, module)) + } + + info, err := i.get(ctx, module, id) + if err != nil { + return nil, err + } + cfg := make(map[string]interface{}) + err = json.Unmarshal([]byte(info.Config), &cfg) + if err != nil { + return nil, err + } + return &dynamic_module_dto.DynamicModule{ + Id: strings.TrimSuffix(info.ID, suffix), + Name: info.Name, + Driver: info.Driver, + Description: info.Description, + Config: cfg, + }, nil +} + +func (i *imlDynamicModule) get(ctx context.Context, module string, id string) (*dynamic_module.DynamicModule, error) { + info, err := i.dynamicModuleService.Get(ctx, id) + if err != nil { + return nil, err + } + + if info.Module != module { + return nil, fmt.Errorf("module not match") + } + return info, nil +} + +func (i *imlDynamicModule) List(ctx context.Context, module string, keyword string, page int, pageSize int) ([]map[string]interface{}, int64, error) { + d, has := driver.Get(module) + if !has { + return nil, 0, fmt.Errorf("module %s not found", module) + } + list, total, err := i.dynamicModuleService.SearchByPage(ctx, keyword, map[string]interface{}{ + "module": module, + }, page, pageSize, "update_at desc") + if err != nil { + return nil, 0, err + } + + userIDs := utils.SliceToSlice(list, func(s *dynamic_module.DynamicModule) string { + return s.Updater + }) + clusters, err := i.clusterService.List(ctx) + if err != nil { + return nil, 0, err + } + + userMap := i.userService.GetLabels(ctx, userIDs...) + items := make([]map[string]interface{}, 0, len(list)) + suffix := fmt.Sprintf("_%s", module) + for _, c := range clusters { + err = i.dynamicClient(ctx, c.Uuid, module, func(dynamicClient gateway.IDynamicClient) error { + versions, err := dynamicClient.Versions(ctx, map[string]string{ + "module": module, + }) + if err != nil { + log.Error("get versions error", err) + } + for _, l := range list { + status := "未发布" + id := strings.TrimSuffix(l.ID, suffix) + + item := map[string]interface{}{ + "id": id, + "title": l.Name, + "driver": l.Driver, + "description": l.Description, + "updater": userMap[l.Updater], + "update_time": l.UpdateAt.Format("2006-01-02 15:04:05"), + } + + tmp := make(map[string]interface{}) + err = json.Unmarshal([]byte(l.Config), &tmp) + if err == nil { + for _, column := range d.Define().Columns() { + if _, ok := item[column]; ok { + continue + } + item[column] = tmp[column] + + } + } + if versions != nil { + if v, ok := versions[strings.ToLower(l.ID)]; ok { + if v == l.Version { + status = "已发布" + } else { + status = "待发布" + } + } + } + item["status"] = status + items = append(items, item) + } + return nil + }) + if err != nil { + return nil, 0, err + } + + } + + return items, total, nil +} diff --git a/module/moduld.go b/module/moduld.go new file mode 100644 index 00000000..60e728a5 --- /dev/null +++ b/module/moduld.go @@ -0,0 +1,5 @@ +// 模块实现 +// 只能使用service下面的接口,不能直接使用store下面的接口 +// + +package module diff --git a/module/my-team/dto/input.go b/module/my-team/dto/input.go new file mode 100644 index 00000000..aff2f9e5 --- /dev/null +++ b/module/my-team/dto/input.go @@ -0,0 +1,16 @@ +package team_dto + +type EditTeam struct { + Name *string `json:"name"` + Description *string `json:"description"` + Master *string `json:"master" aocheck:"user"` +} + +type UserIDs struct { + Users []string `json:"users"` +} + +type UpdateMemberRole struct { + Roles []string `json:"roles" aocheck:"role"` + Users []string `json:"users" aocheck:"user"` +} diff --git a/module/my-team/dto/output.go b/module/my-team/dto/output.go new file mode 100644 index 00000000..d5232631 --- /dev/null +++ b/module/my-team/dto/output.go @@ -0,0 +1,82 @@ +package team_dto + +import ( + "github.com/APIParkLab/APIPark/service/team" + team_member "github.com/APIParkLab/APIPark/service/team-member" + "github.com/eolinker/go-common/auto" +) + +type Item struct { + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + CreateTime auto.TimeLabel `json:"create_time"` + UpdateTime auto.TimeLabel `json:"update_time"` + ServiceNum int64 `json:"service_num"` + AppNum int64 `json:"app_num"` + CanDelete bool `json:"can_delete"` +} + +func ToItem(model *team.Team, serviceNum int64, appNum int64) *Item { + return &Item{ + Id: model.Id, + Name: model.Name, + Description: model.Description, + CreateTime: auto.TimeLabel(model.CreateTime), + UpdateTime: auto.TimeLabel(model.UpdateTime), + ServiceNum: serviceNum, + AppNum: appNum, + CanDelete: serviceNum == 0 && appNum == 0, + } +} + +type SimpleTeam struct { + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + ServiceNum int64 `json:"service_num"` + AppNum int64 `json:"app_num"` +} + +type Team struct { + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + CreateTime auto.TimeLabel `json:"create_time"` + UpdateTime auto.TimeLabel `json:"update_time"` + Creator auto.Label `json:"creator" aolabel:"user"` + Updater auto.Label `json:"updater" aolabel:"user"` +} + +func ToTeam(model *team.Team) *Team { + return &Team{ + Id: model.Id, + Name: model.Name, + Description: model.Description, + CreateTime: auto.TimeLabel(model.CreateTime), + UpdateTime: auto.TimeLabel(model.UpdateTime), + Creator: auto.UUID(model.Creator), + Updater: auto.UUID(model.Updater), + } +} + +type Member struct { + User auto.Label `json:"user" aolabel:"user"` + Roles []auto.Label `json:"roles" aolabel:"role"` + AttachTime auto.TimeLabel `json:"attach_time"` +} + +func ToMember(model *team_member.Member, roles ...string) *Member { + + return &Member{ + User: auto.UUID(model.UID), + Roles: auto.List(roles), + AttachTime: auto.TimeLabel(model.CreateTime), + } +} + +type SimpleMember struct { + User auto.Label `json:"user" aolabel:"user"` + Mail string `json:"mail"` + Department []auto.Label `json:"department"` +} diff --git a/module/my-team/iml.go b/module/my-team/iml.go new file mode 100644 index 00000000..016866a5 --- /dev/null +++ b/module/my-team/iml.go @@ -0,0 +1,349 @@ +package my_team + +import ( + "context" + "errors" + "fmt" + + "github.com/eolinker/ap-account/service/role" + + "gorm.io/gorm" + + department_member "github.com/eolinker/ap-account/service/department-member" + "github.com/eolinker/go-common/auto" + + "github.com/eolinker/ap-account/service/user" + + "github.com/eolinker/go-common/store" + + "github.com/APIParkLab/APIPark/service/service" + team_member "github.com/APIParkLab/APIPark/service/team-member" + + team_dto "github.com/APIParkLab/APIPark/module/my-team/dto" + "github.com/APIParkLab/APIPark/service/team" + "github.com/eolinker/go-common/utils" +) + +var ( + _ ITeamModule = (*imlTeamModule)(nil) +) + +type imlTeamModule struct { + teamService team.ITeamService `autowired:""` + teamMemberService team_member.ITeamMemberService `autowired:""` + roleService role.IRoleService `autowired:""` + roleMemberService role.IRoleMemberService `autowired:""` + userService user.IUserService `autowired:""` + departmentMemberService department_member.IMemberService `autowired:""` + serviceService service.IServiceService `autowired:""` + transaction store.ITransaction `autowired:""` +} + +func (m *imlTeamModule) UpdateMemberRole(ctx context.Context, id string, input *team_dto.UpdateMemberRole) error { + _, err := m.teamService.Get(ctx, id) + if err != nil { + return err + } + return m.transaction.Transaction(ctx, func(ctx context.Context) error { + if len(input.Roles) < 1 { + return errors.New("at least one role") + } + err = m.roleMemberService.RemoveUserRole(ctx, role.TeamTarget(id), input.Users...) + if err != nil { + return err + } + for _, roleId := range input.Roles { + for _, userId := range input.Users { + err = m.roleMemberService.Add(ctx, &role.AddMember{ + Role: roleId, + User: userId, + Target: role.TeamTarget(id), + }) + if err != nil { + return err + } + } + } + return nil + }) +} + +func (m *imlTeamModule) GetTeam(ctx context.Context, id string) (*team_dto.Team, error) { + tv, err := m.teamService.Get(ctx, id) + if err != nil { + return nil, err + } + + return &team_dto.Team{ + Id: tv.Id, + Name: tv.Name, + Description: tv.Description, + CreateTime: auto.TimeLabel(tv.CreateTime), + UpdateTime: auto.TimeLabel(tv.UpdateTime), + Creator: auto.UUID(tv.Creator), + Updater: auto.UUID(tv.Updater), + }, nil +} + +func (m *imlTeamModule) Search(ctx context.Context, keyword string) ([]*team_dto.Item, error) { + userID := utils.UserId(ctx) + memberMap, err := m.teamMemberService.FilterMembersForUser(ctx, userID) + if err != nil { + return nil, err + } + teamIDs, ok := memberMap[userID] + if !ok || len(teamIDs) == 0 { + return make([]*team_dto.Item, 0), nil + } + list, err := m.teamService.Search(ctx, keyword, map[string]interface{}{ + "uuid": teamIDs, + }) + if err != nil { + return nil, err + } + serviceNumMap, err := m.serviceService.ServiceCountByTeam(ctx, teamIDs...) + if err != nil { + return nil, err + } + appNumMap, err := m.serviceService.AppCountByTeam(ctx, teamIDs...) + if err != nil { + return nil, err + } + + outList := make([]*team_dto.Item, 0, len(list)) + for _, v := range list { + outList = append(outList, team_dto.ToItem(v, serviceNumMap[v.Id], appNumMap[v.Id])) + } + return outList, nil +} + +func (m *imlTeamModule) Edit(ctx context.Context, id string, input *team_dto.EditTeam) (*team_dto.Team, error) { + err := m.transaction.Transaction(ctx, func(ctx context.Context) error { + if input.Master != nil { + // 负责人是否在团队内,若不在,则新增 + members, err := m.teamMemberService.Members(ctx, []string{id}, []string{*input.Master}) + if err != nil { + return err + } + if len(members) == 0 { + err = m.teamMemberService.AddMemberTo(ctx, id, *input.Master) + if err != nil { + return err + } + } + } + return m.teamService.Save(ctx, id, &team.EditTeam{ + Name: input.Name, + Description: input.Description, + }) + }) + + if err != nil { + return nil, err + } + return m.GetTeam(ctx, id) +} + +func (m *imlTeamModule) SimpleTeams(ctx context.Context, keyword string) ([]*team_dto.SimpleTeam, error) { + userID := utils.UserId(ctx) + memberMap, err := m.teamMemberService.FilterMembersForUser(ctx, userID) + if err != nil { + return nil, err + } + teamIDs, ok := memberMap[userID] + if !ok || len(teamIDs) == 0 { + return make([]*team_dto.SimpleTeam, 0), nil + } + list, err := m.teamService.Search(ctx, keyword, map[string]interface{}{ + "uuid": teamIDs, + }) + if err != nil { + return nil, err + } + + projects, err := m.serviceService.Search(ctx, "", map[string]interface{}{ + "team": teamIDs, + }) + projectCount := make(map[string]int64) + appCount := make(map[string]int64) + for _, p := range projects { + if p.AsServer { + if _, ok := projectCount[p.Team]; !ok { + projectCount[p.Team] = 0 + } + projectCount[p.Team]++ + } + if p.AsApp { + if _, ok := appCount[p.Team]; !ok { + appCount[p.Team] = 0 + } + appCount[p.Team]++ + } + } + + outList := utils.SliceToSlice(list, func(s *team.Team) *team_dto.SimpleTeam { + return &team_dto.SimpleTeam{ + Id: s.Id, + Name: s.Name, + Description: s.Description, + ServiceNum: projectCount[s.Id], + AppNum: appCount[s.Id], + } + }) + return outList, nil +} + +func (m *imlTeamModule) AddMember(ctx context.Context, id string, uuids ...string) error { + _, err := m.teamService.Get(ctx, id) + if err != nil { + return err + } + return m.transaction.Transaction(ctx, func(ctx context.Context) error { + err = m.teamMemberService.AddMemberTo(ctx, id, uuids...) + if err != nil { + return err + } + r, err := m.roleService.GetDefaultRole(ctx, role.GroupTeam) + if err != nil { + return err + } + for _, uid := range uuids { + err = m.roleMemberService.Add(ctx, &role.AddMember{Role: r.Id, User: uid, Target: role.TeamTarget(id)}) + if err != nil { + return err + } + } + return nil + }) + +} + +func (m *imlTeamModule) RemoveMember(ctx context.Context, id string, uuids ...string) error { + _, err := m.teamService.Get(ctx, id) + if err != nil { + return err + } + + supperRole, err := m.roleService.GetSupperRole(ctx, role.GroupTeam) + if err != nil { + return err + } + count, err := m.roleMemberService.CountByRole(ctx, role.TeamTarget(id), supperRole.Id) + if err != nil { + return err + } + members, err := m.roleMemberService.List(ctx, role.TeamTarget(id), uuids...) + if err != nil { + return err + } + if len(members) >= int(count) { + supperRoleCount := 0 + for _, member := range members { + if member.Role == supperRole.Id { + supperRoleCount++ + } + } + + if supperRoleCount == int(count) { + return errors.New("can not delete all team admin") + } + } + + return m.transaction.Transaction(ctx, func(ctx context.Context) error { + err = m.roleMemberService.RemoveUserRole(ctx, role.TeamTarget(id), uuids...) + if err != nil { + return err + } + return m.teamMemberService.RemoveMemberFrom(ctx, id, uuids...) + }) + +} + +func (m *imlTeamModule) Members(ctx context.Context, id string, keyword string) ([]*team_dto.Member, error) { + _, err := m.teamService.Get(ctx, id) + if err != nil { + return nil, err + } + users, err := m.userService.Search(ctx, keyword, -1) + if err != nil { + return nil, err + } + if len(users) == 0 { + return make([]*team_dto.Member, 0), nil + } + userIds := utils.SliceToSlice(users, func(s *user.User) string { + return s.UID + }) + members, err := m.teamMemberService.Members(ctx, []string{id}, userIds) + if err != nil { + return nil, err + } + roleMembers, err := m.roleMemberService.List(ctx, role.TeamTarget(id)) + if err != nil { + return nil, err + } + roleMemberMap := utils.SliceToMapArrayO(roleMembers, func(r *role.Member) (string, string) { + return r.User, r.Role + }) + + out := make([]*team_dto.Member, 0, len(members)) + for _, member := range members { + out = append(out, team_dto.ToMember(member, roleMemberMap[member.UID]...)) + } + + return out, nil +} + +func (m *imlTeamModule) SimpleMembers(ctx context.Context, id string, keyword string) ([]*team_dto.SimpleMember, error) { + if id == "" { + return nil, fmt.Errorf("team id is empty") + } + teamInfo, err := m.teamService.Get(ctx, id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + if teamInfo == nil { + return nil, fmt.Errorf("team %s not extist", id) + } + users, err := m.userService.Search(ctx, keyword, -1) + if err != nil { + return nil, err + } + userMap := make(map[string]*user.User) + userIds := make([]string, 0, len(users)) + for _, u := range users { + userIds = append(userIds, u.UID) + userMap[u.UID] = u + } + teamMembers, err := m.teamMemberService.Members(ctx, []string{id}, userIds) + if err != nil { + return nil, err + } + departmentMembers, err := m.departmentMemberService.Members(ctx, nil, userIds) + if err != nil { + return nil, err + } + departmentMemberMap := make(map[string][]string) + for _, member := range departmentMembers { + if _, ok := departmentMemberMap[member.UID]; !ok { + departmentMemberMap[member.UID] = make([]string, 0) + } + departmentMemberMap[member.UID] = append(departmentMemberMap[member.UID], member.Come) + } + + out := make([]*team_dto.SimpleMember, 0, len(teamMembers)) + for _, member := range teamMembers { + u, ok := userMap[member.UID] + if !ok { + continue + } + + out = append(out, &team_dto.SimpleMember{ + User: auto.UUID(u.UID), + Mail: u.Email, + Department: auto.List(departmentMemberMap[member.UID]), + }) + } + + return out, nil +} diff --git a/module/my-team/team.go b/module/my-team/team.go new file mode 100644 index 00000000..59b302d8 --- /dev/null +++ b/module/my-team/team.go @@ -0,0 +1,38 @@ +package my_team + +import ( + "context" + "reflect" + + "github.com/eolinker/go-common/autowire" + + team_dto "github.com/APIParkLab/APIPark/module/my-team/dto" +) + +type ITeamModule interface { + // GetTeam 获取团队信息 + GetTeam(ctx context.Context, id string) (*team_dto.Team, error) + // Search 搜索团队 + Search(ctx context.Context, keyword string) ([]*team_dto.Item, error) + // Edit 编辑团队 + Edit(ctx context.Context, id string, input *team_dto.EditTeam) (*team_dto.Team, error) + // SimpleTeams 简易搜索团队 + SimpleTeams(ctx context.Context, keyword string) ([]*team_dto.SimpleTeam, error) + // AddMember 添加团队成员 + AddMember(ctx context.Context, id string, uuids ...string) error + // RemoveMember 移除团队成员 + RemoveMember(ctx context.Context, id string, uuids ...string) error + // Members 获取团队成员列表 + Members(ctx context.Context, id string, keyword string) ([]*team_dto.Member, error) + // SimpleMembers 获取团队成员简易列表 + SimpleMembers(ctx context.Context, id string, keyword string) ([]*team_dto.SimpleMember, error) + + // UpdateMemberRole 更新成员角色 + UpdateMemberRole(ctx context.Context, id string, input *team_dto.UpdateMemberRole) error +} + +func init() { + autowire.Auto[ITeamModule](func() reflect.Value { + return reflect.ValueOf(new(imlTeamModule)) + }) +} diff --git a/module/permit/dto/input.go b/module/permit/dto/input.go new file mode 100644 index 00000000..5164406c --- /dev/null +++ b/module/permit/dto/input.go @@ -0,0 +1,6 @@ +package permit_dto + +type SetGrant struct { + Access string `json:"access"` + Key string `json:"key"` +} diff --git a/module/permit/dto/output.go b/module/permit/dto/output.go new file mode 100644 index 00000000..bd31bc97 --- /dev/null +++ b/module/permit/dto/output.go @@ -0,0 +1,30 @@ +package permit_dto + +import ( + "github.com/APIParkLab/APIPark/service/permit-type" + "strings" +) + +type Permission struct { + Access string `json:"access"` + Name string `json:"name"` + Description string `json:"description"` + Grant []*Grant `json:"grant"` +} +type Grant = permit_type.Target + +type Option = Grant + +func SearchOptions(ops []*Option, keyword string) []*Option { + if keyword == "" { + return ops + } + rs := make([]*Option, 0, len(ops)) + + for _, op := range ops { + if op.Name == keyword || strings.Index(op.Name, keyword) > -1 || op.Label == keyword || strings.Index(op.Label, keyword) > -1 { + rs = append(rs, op) + } + } + return rs +} diff --git a/module/permit/system/iml.go b/module/permit/system/iml.go new file mode 100644 index 00000000..2cd4d3c5 --- /dev/null +++ b/module/permit/system/iml.go @@ -0,0 +1,78 @@ +package system + +import ( + "context" + "errors" + "reflect" + + "github.com/gin-gonic/gin" + + "github.com/eolinker/ap-account/service/role" + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/permit" + "github.com/eolinker/go-common/utils" +) + +var ( + _ ISystemPermitModule = (*imlSystemPermitModule)(nil) + _ autowire.Complete = (*imlSystemPermitModule)(nil) +) + +type imlSystemPermitModule struct { + permitService permit.IPermit `autowired:""` + roleService role.IRoleService `autowired:""` + roleMemberService role.IRoleMemberService `autowired:""` +} + +func (m *imlSystemPermitModule) accesses(ctx context.Context) ([]string, error) { + uid := utils.UserId(ctx) + if uid == "" { + return nil, errors.New("not login") + } + roleMembers, err := m.roleMemberService.List(ctx, role.SystemTarget(), uid) + if err != nil { + return nil, err + } + if len(roleMembers) == 0 { + return []string{}, nil + } + roleIds := utils.SliceToSlice(roleMembers, func(rm *role.Member) string { + return rm.Role + }) + roles, err := m.roleService.List(ctx, roleIds...) + if err != nil { + return nil, err + } + permits := make(map[string]struct{}) + for _, r := range roles { + for _, p := range r.Permit { + permits[p] = struct{}{} + } + } + return utils.MapToSlice(permits, func(k string, v struct{}) string { + return k + }), nil +} + +func (m *imlSystemPermitModule) Permissions(ctx context.Context) ([]string, error) { + return m.accesses(ctx) +} + +func (m *imlSystemPermitModule) domain(ctx *gin.Context) ([]string, []string, bool) { + + system, err := m.accesses(ctx) + if err != nil { + return nil, nil, false + } + return []string{role.GroupSystem}, system, true +} + +func (m *imlSystemPermitModule) OnComplete() { + permit.AddDomainHandler(role.GroupSystem, m.domain) +} + +func init() { + autowire.Auto[ISystemPermitModule](func() reflect.Value { + return reflect.ValueOf(new(imlSystemPermitModule)) + }) +} diff --git a/module/permit/system/module.go b/module/permit/system/module.go new file mode 100644 index 00000000..4c7f62ce --- /dev/null +++ b/module/permit/system/module.go @@ -0,0 +1,18 @@ +package system + +import ( + "context" + "reflect" + + "github.com/eolinker/go-common/autowire" +) + +type ISystemPermitModule interface { + Permissions(ctx context.Context) ([]string, error) +} + +func init() { + autowire.Auto[ISystemPermitModule](func() reflect.Value { + return reflect.ValueOf(new(imlSystemPermitModule)) + }) +} diff --git a/module/permit/team/iml.go b/module/permit/team/iml.go new file mode 100644 index 00000000..a624f804 --- /dev/null +++ b/module/permit/team/iml.go @@ -0,0 +1,99 @@ +package team + +import ( + "context" + "errors" + + "github.com/eolinker/go-common/permit" + + "github.com/gin-gonic/gin" + + "github.com/eolinker/ap-account/service/role" + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/utils" +) + +var ( + _ ITeamPermitModule = (*imlTeamPermitModule)(nil) + _ autowire.Complete = (*imlTeamPermitModule)(nil) +) + +type imlTeamPermitModule struct { + roleService role.IRoleService `autowired:""` + roleMemberService role.IRoleMemberService `autowired:""` +} + +func (m *imlTeamPermitModule) Permissions(ctx context.Context, teamId string) ([]string, error) { + + uid := utils.UserId(ctx) + roleMembers, err := m.roleMemberService.List(ctx, role.TeamTarget(teamId), uid) + if err != nil { + return nil, err + } + roleIds := utils.SliceToSlice(roleMembers, func(rm *role.Member) string { + return rm.Role + }) + if len(roleMembers) == 0 { + return []string{}, nil + } + roles, err := m.roleService.List(ctx, roleIds...) + if err != nil { + return nil, err + } + permits := make(map[string]struct{}) + for _, r := range roles { + for _, p := range r.Permit { + permits[p] = struct{}{} + } + } + + return utils.MapToSlice(permits, func(k string, v struct{}) string { + return k + }), nil +} + +func (m *imlTeamPermitModule) OnComplete() { + permit.AddDomainHandler(role.GroupTeam, m.domain) +} + +func (m *imlTeamPermitModule) accesses(ctx context.Context, teamId string) ([]string, error) { + uid := utils.UserId(ctx) + if uid == "" { + return nil, errors.New("not login") + } + roleMembers, err := m.roleMemberService.List(ctx, role.TeamTarget(teamId), uid) + if err != nil { + return nil, err + } + if len(roleMembers) == 0 { + return []string{}, nil + } + roleIds := utils.SliceToSlice(roleMembers, func(rm *role.Member) string { + return rm.Role + }) + roles, err := m.roleService.List(ctx, roleIds...) + if err != nil { + return nil, err + } + permits := make(map[string]struct{}) + for _, r := range roles { + for _, p := range r.Permit { + permits[p] = struct{}{} + } + } + return utils.MapToSlice(permits, func(k string, v struct{}) string { + return k + }), nil +} + +func (m *imlTeamPermitModule) domain(ctx *gin.Context) ([]string, []string, bool) { + teamId := ctx.Query("team") + if teamId == "" { + return nil, nil, false + } + accesses, err := m.accesses(ctx, teamId) + if err != nil { + return nil, nil, false + } + return []string{role.GroupTeam}, accesses, true +} diff --git a/module/permit/team/module.go b/module/permit/team/module.go new file mode 100644 index 00000000..867d8f33 --- /dev/null +++ b/module/permit/team/module.go @@ -0,0 +1,28 @@ +package team + +import ( + "context" + "reflect" + + "github.com/eolinker/go-common/autowire" +) + +const ( + accessGroup = "team" +) + +type ITeamPermitModule interface { + Permissions(ctx context.Context, teamId string) ([]string, error) +} + +func init() { + var m *imlTeamPermitModule + + autowire.Auto[ITeamPermitModule](func() reflect.Value { + if m == nil { + m = new(imlTeamPermitModule) + } + return reflect.ValueOf(m) + }) + +} diff --git a/module/plugin-cluster/dto/input.go b/module/plugin-cluster/dto/input.go new file mode 100644 index 00000000..c03927eb --- /dev/null +++ b/module/plugin-cluster/dto/input.go @@ -0,0 +1,8 @@ +package dto + +import "github.com/APIParkLab/APIPark/model/plugin_model" + +type PluginSetting struct { + Status plugin_model.Status `json:"status"` + Config plugin_model.ConfigType `json:"config"` +} diff --git a/module/plugin-cluster/dto/plugin.go b/module/plugin-cluster/dto/plugin.go new file mode 100644 index 00000000..49aa6f0f --- /dev/null +++ b/module/plugin-cluster/dto/plugin.go @@ -0,0 +1,44 @@ +package dto + +import ( + "github.com/APIParkLab/APIPark/model/plugin_model" + "github.com/eolinker/go-common/auto" +) + +type Item struct { + Name string `json:"name"` + Cname string `json:"cname"` + Extend string `json:"extend"` + Desc string `json:"desc"` + Operator *auto.Label `json:"operator,omitempty" aolabel:"operator"` + Update *auto.TimeLabel `json:"update,omitempty"` + Create *auto.TimeLabel `json:"create,omitempty"` +} +type Define struct { + Name string `json:"name"` + Cname string `json:"cname"` + Extend string `json:"extend"` + Desc string `json:"desc"` + Render plugin_model.Render `json:"render"` + Default plugin_model.ConfigType `json:"default"` +} +type PluginOutput struct { + //Cluster auto.Label `json:"partition,omitempty" aolabel:"partition"` + Name string `json:"name"` + Cname string `json:"cname"` + Extend string `json:"extend"` + Desc string `json:"desc"` + Status plugin_model.Status `json:"status"` + Config plugin_model.ConfigType `json:"config"` + Operator *auto.Label `json:"operator,omitempty" aolabel:"operator"` + Update *auto.TimeLabel `json:"update,omitempty"` + Create *auto.TimeLabel `json:"create,omitempty"` +} + +type PluginOption struct { + Name string `json:"name"` + Cname string `json:"cname"` + Desc string `json:"desc"` + Default plugin_model.ConfigType `json:"default"` + Render plugin_model.Render `json:"render"` +} diff --git a/module/plugin-cluster/iml.go b/module/plugin-cluster/iml.go new file mode 100644 index 00000000..15d22391 --- /dev/null +++ b/module/plugin-cluster/iml.go @@ -0,0 +1,174 @@ +package plugin_cluster + +import ( + "context" + "fmt" + + plugin_cluster "github.com/APIParkLab/APIPark/service/plugin-cluster" + + "github.com/APIParkLab/APIPark/gateway" + "github.com/APIParkLab/APIPark/model/plugin_model" + "github.com/APIParkLab/APIPark/module/plugin-cluster/dto" + "github.com/APIParkLab/APIPark/service/cluster" + "github.com/eolinker/eosc/log" + "github.com/eolinker/go-common/auto" + "github.com/eolinker/go-common/utils" +) + +var ( + _ IPluginClusterModule = (*imlPluginClusterModule)(nil) +) + +type imlPluginClusterModule struct { + service plugin_cluster.IPluginService `autowired:""` + //partitionService partition.IPartitionService `autowired:""` + clusterService cluster.IClusterService `autowired:""` +} + +func (m *imlPluginClusterModule) UpdateDefine(ctx context.Context, defines []*plugin_model.Define) error { + err := m.service.SaveDefine(ctx, defines) + if err != nil { + return err + } + return m.initAllCluster(ctx) +} +func (m *imlPluginClusterModule) initAllCluster(ctx context.Context) error { + + clusters, err := m.clusterService.List(ctx) + if err != nil { + return err + } + + for _, c := range clusters { + err := m.initCluster(ctx, c.Uuid) + if err != nil { + log.Warn("init cluster:%s %s", c.Name, err.Error()) + } + } + return nil +} +func (m *imlPluginClusterModule) initGateway(ctx context.Context, clusterId string, clientDriver gateway.IClientDriver) error { + configForPartitions, err := m.service.ListCluster(ctx, clusterId) + if err != nil { + return err + } + pluginConfigs := utils.SliceToSlice(configForPartitions, func(s *plugin_cluster.ConfigPartition) *gateway.PluginConfig { + + return &gateway.PluginConfig{ + Id: s.Extend, + Name: s.Plugin, + Config: s.Config.Config, + Status: s.Status.String(), + } + }) + + return clientDriver.PluginSetting().Set(ctx, pluginConfigs) +} +func (m *imlPluginClusterModule) GetDefine(ctx context.Context, name string) (*dto.Define, error) { + define, err := m.service.GetDefine(ctx, name) + if err != nil { + return nil, err + } + return &dto.Define{ + Name: define.Name, + Cname: define.Cname, + Desc: define.Desc, + Default: define.Config, + Render: define.Render, + Extend: define.Extend, + }, nil +} + +func (m *imlPluginClusterModule) Options(ctx context.Context) ([]*dto.PluginOption, error) { + defines, err := m.service.Defines(ctx, plugin_model.OpenKind) + if err != nil { + return nil, err + } + + return utils.SliceToSlice(defines, func(s *plugin_cluster.PluginDefine) *dto.PluginOption { + return &dto.PluginOption{ + Name: s.Name, + Cname: s.Cname, + Desc: s.Desc, + Default: s.Config, + Render: s.Render, + } + }), nil +} + +func (m *imlPluginClusterModule) List(ctx context.Context, clusterId string) ([]*dto.Item, error) { + + configPartitions, err := m.service.ListCluster(ctx, clusterId, plugin_model.OpenKind) + if err != nil { + return nil, err + } + return utils.SliceToSlice(configPartitions, func(s *plugin_cluster.ConfigPartition) *dto.Item { + return &dto.Item{ + + Name: s.Plugin, + Cname: s.Cname, + Desc: s.Desc, + Extend: s.Extend, + Operator: auto.UUIDP(s.Operator), + Update: (*auto.TimeLabel)(s.Update), + Create: (*auto.TimeLabel)(s.Create), + } + }), nil +} + +func (m *imlPluginClusterModule) Get(ctx context.Context, clusterId string, name string) (config *dto.PluginOutput, render plugin_model.Render, er error) { + if clusterId == "" { + return nil, nil, fmt.Errorf("partition is require") + } + cf, define, err := m.service.GetConfig(ctx, clusterId, name) + if err != nil { + return nil, nil, err + } + if define.Kind != plugin_model.OpenKind { + return nil, nil, fmt.Errorf("plugin %s [extend:%s] not support for setting ", name, define.Extend) + } + out := &dto.PluginOutput{ + //Cluster: auto.UUID(cf.Cluster), + Name: cf.Plugin, + Cname: define.Cname, + Extend: define.Extend, + Desc: define.Desc, + Status: cf.Status, + Config: cf.Config, + } + if cf.Operator != "" { + out.Operator = auto.UUIDP(cf.Operator) + } + if cf.Create != nil { + out.Create = (*auto.TimeLabel)(cf.Create) + } + if cf.Update != nil { + out.Update = (*auto.TimeLabel)(cf.Update) + } + return out, define.Render, nil + +} + +func (m *imlPluginClusterModule) Set(ctx context.Context, clusterId string, name string, config *dto.PluginSetting) error { + + err := m.service.SetCluster(ctx, clusterId, name, config.Status, config.Config) + if err != nil { + return err + } + + return m.initCluster(ctx, clusterId) +} + +func (m *imlPluginClusterModule) initCluster(ctx context.Context, clusterId string) error { + client, err := m.clusterService.GatewayClient(ctx, clusterId) + if err != nil { + return err + } + defer func() { + err := client.Close(ctx) + if err != nil { + log.Warn("close apinto client:", err) + } + }() + return m.initGateway(ctx, clusterId, client) +} diff --git a/module/plugin-cluster/plugin-partition.go b/module/plugin-cluster/plugin-partition.go new file mode 100644 index 00000000..d76e3107 --- /dev/null +++ b/module/plugin-cluster/plugin-partition.go @@ -0,0 +1,28 @@ +package plugin_cluster + +import ( + "context" + "reflect" + + "github.com/APIParkLab/APIPark/gateway" + "github.com/APIParkLab/APIPark/model/plugin_model" + "github.com/APIParkLab/APIPark/module/plugin-cluster/dto" + "github.com/eolinker/go-common/autowire" +) + +type IPluginClusterModule interface { + List(ctx context.Context, clusterId string) ([]*dto.Item, error) + Get(ctx context.Context, clusterId string, name string) (config *dto.PluginOutput, render plugin_model.Render, er error) + Set(ctx context.Context, clusterId string, name string, config *dto.PluginSetting) error + Options(ctx context.Context) ([]*dto.PluginOption, error) + GetDefine(ctx context.Context, name string) (*dto.Define, error) + UpdateDefine(ctx context.Context, defines []*plugin_model.Define) error +} + +func init() { + autowire.Auto[IPluginClusterModule](func() reflect.Value { + m := new(imlPluginClusterModule) + gateway.RegisterInitHandleFunc(m.initGateway) + return reflect.ValueOf(m) + }) +} diff --git a/module/publish/dto/diff.go b/module/publish/dto/diff.go new file mode 100644 index 00000000..471ed0ad --- /dev/null +++ b/module/publish/dto/diff.go @@ -0,0 +1,5 @@ +package dto + +import service_diff "github.com/APIParkLab/APIPark/module/service-diff" + +type DiffOut = service_diff.DiffOut diff --git a/module/publish/dto/in.go b/module/publish/dto/in.go new file mode 100644 index 00000000..c874d81f --- /dev/null +++ b/module/publish/dto/in.go @@ -0,0 +1,18 @@ +package dto + +type ApplyOnReleaseInput struct { + Version string `json:"version"` + VersionRemark string `json:"version_remark"` + PublishRemark string `json:"remark"` +} + +type ApplyInput struct { + Release string `json:"release"` + Remark string `json:"remark"` +} + +type Comments struct { + Comments string `json:"comments"` +} +type DoPublish struct { +} diff --git a/module/publish/dto/out.go b/module/publish/dto/out.go new file mode 100644 index 00000000..bcfa4d37 --- /dev/null +++ b/module/publish/dto/out.go @@ -0,0 +1,60 @@ +package dto + +import ( + "github.com/APIParkLab/APIPark/service/publish" + "github.com/eolinker/go-common/auto" +) + +type Publish struct { + Id string `json:"id,omitempty"` + Version string `json:"version,omitempty"` + Remark string `json:"remark,omitempty"` + VersionRemark string `json:"version_remark,omitempty"` + Comments string `json:"comments,omitempty"` + Release auto.Label `json:"release,omitempty" aolabel:"release"` + Previous *auto.Label `json:"previous,omitempty" aolabel:"release"` + Service auto.Label `json:"service" aolabel:"service"` + Applicant auto.Label `json:"applicant" aolabel:"user"` + Approver *auto.Label `json:"approver,omitempty" aolabel:"user"` + Status publish.StatusType `json:"status,omitempty" ` + ApplyTIme auto.TimeLabel `json:"apply_time" ` + ApproveTime auto.TimeLabel `json:"approve_time"` +} + +func FromModel(m *publish.Publish, versionRemark string) *Publish { + + p := &Publish{ + Id: m.Id, + Version: m.Version, + Remark: m.Remark, + VersionRemark: versionRemark, + Comments: m.Comments, + Service: auto.UUID(m.Service), + Applicant: auto.UUID(m.Applicant), + Release: auto.UUID(m.Release), + + Status: m.Status, + ApplyTIme: auto.TimeLabel(m.ApplyTime), + ApproveTime: auto.TimeLabel(m.ApproveTime), + } + if m.Approver != "" { + p.Approver = auto.UUIDP(m.Approver) + } + if m.Previous != "" { + p.Previous = auto.UUIDP(m.Previous) + } + return p +} + +type PublishDetail struct { + *Publish + Diffs *DiffOut `json:"diffs"` + PublishStatuses []*PublishStatus `json:"cluster_publish_status"` +} + +type PublishStatus struct { + //Partition auto.Label `json:"partition" aolabel:"partition"` + //Cluster auto.Label `json:"cluster" aolabel:"cluster"` + Status string `json:"status"` + Error string `json:"error"` +} diff --git a/module/publish/iml.go b/module/publish/iml.go new file mode 100644 index 00000000..48b0119f --- /dev/null +++ b/module/publish/iml.go @@ -0,0 +1,616 @@ +package publish + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/eolinker/go-common/store" + + "github.com/APIParkLab/APIPark/service/service" + + "github.com/APIParkLab/APIPark/service/universally/commit" + + "github.com/APIParkLab/APIPark/service/api" + "github.com/APIParkLab/APIPark/service/upstream" + + "github.com/APIParkLab/APIPark/gateway" + + "github.com/eolinker/eosc/log" + + "github.com/APIParkLab/APIPark/module/publish/dto" + releaseModule "github.com/APIParkLab/APIPark/module/release" + serviceDiff "github.com/APIParkLab/APIPark/module/service-diff" + "github.com/APIParkLab/APIPark/service/cluster" + "github.com/APIParkLab/APIPark/service/publish" + "github.com/APIParkLab/APIPark/service/release" + "github.com/eolinker/go-common/utils" + "github.com/google/uuid" + "gorm.io/gorm" +) + +var ( + _ IPublishModule = (*imlPublishModule)(nil) + asServer = map[string]bool{ + "as_server": true, + } +) + +type imlPublishModule struct { + projectDiffModule serviceDiff.IServiceDiffModule `autowired:""` + releaseModule releaseModule.IReleaseModule `autowired:""` + publishService publish.IPublishService `autowired:""` + apiService api.IAPIService `autowired:""` + upstreamService upstream.IUpstreamService `autowired:""` + releaseService release.IReleaseService `autowired:""` + clusterService cluster.IClusterService `autowired:""` + serviceService service.IServiceService `autowired:""` + transaction store.ITransaction `autowired:""` +} + +func (m *imlPublishModule) initGateway(ctx context.Context, partitionId string, clientDriver gateway.IClientDriver) error { + + projects, err := m.serviceService.List(ctx) + if err != nil { + return err + } + projectIds := utils.SliceToSlice(projects, func(p *service.Service) string { + return p.Id + }) + for _, projectId := range projectIds { + releaseInfo, err := m.getProjectRelease(ctx, projectId, partitionId) + if err != nil { + return err + } + if releaseInfo == nil { + continue + } + + err = clientDriver.Project().Online(ctx, releaseInfo) + if err != nil { + return err + } + } + return nil +} + +func (m *imlPublishModule) getProjectRelease(ctx context.Context, projectID string, partitionId string) (*gateway.ProjectRelease, error) { + + releaseInfo, err := m.releaseService.GetRunning(ctx, projectID) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + return nil, nil + } + commits, err := m.releaseService.GetCommits(ctx, releaseInfo.UUID) + if err != nil { + return nil, err + } + apiIds := make([]string, 0, len(commits)) + apiProxyCommitIds := make([]string, 0, len(commits)) + upstreamCommitIds := make([]string, 0, len(commits)) + for _, c := range commits { + switch c.Type { + case release.CommitApiProxy: + apiIds = append(apiIds, c.Target) + apiProxyCommitIds = append(apiProxyCommitIds, c.Commit) + case release.CommitUpstream: + upstreamCommitIds = append(upstreamCommitIds, c.Commit) + } + } + + apiInfos, err := m.apiService.ListInfo(ctx, apiIds...) + if err != nil { + return nil, err + } + + proxyCommits, err := m.apiService.ListProxyCommit(ctx, apiProxyCommitIds...) + if err != nil { + return nil, err + } + proxyCommitMap := utils.SliceToMapO(proxyCommits, func(c *commit.Commit[api.Proxy]) (string, *api.Proxy) { + return c.Target, c.Data + }) + + upstreamCommits, err := m.upstreamService.ListCommit(ctx, upstreamCommitIds...) + if err != nil { + return nil, err + } + version := releaseInfo.UUID + apis := make([]*gateway.ApiRelease, 0, len(apiInfos)) + for _, a := range apiInfos { + apiInfo := &gateway.ApiRelease{ + BasicItem: &gateway.BasicItem{ + ID: a.UUID, + Description: a.Description, + Version: version, + }, + Path: a.Path, + Method: []string{a.Method}, + Service: a.Upstream, + } + proxy, ok := proxyCommitMap[a.UUID] + if ok { + apiInfo.ProxyPath = proxy.Path + apiInfo.ProxyHeaders = utils.SliceToSlice(proxy.Headers, func(h *api.Header) *gateway.ProxyHeader { + return &gateway.ProxyHeader{ + Key: h.Key, + Value: h.Value, + } + }) + apiInfo.Retry = proxy.Retry + apiInfo.Timeout = proxy.Timeout + } + apis = append(apis, apiInfo) + } + var upstreamRelease *gateway.UpstreamRelease + for _, c := range upstreamCommits { + if c.Key != partitionId { + continue + } + upstreamRelease = &gateway.UpstreamRelease{ + BasicItem: &gateway.BasicItem{ + ID: c.Target, + Version: version, + MatchLabels: map[string]string{ + "serviceId": projectID, + }, + }, + PassHost: c.Data.PassHost, + Scheme: c.Data.Scheme, + Balance: c.Data.Balance, + Timeout: c.Data.Timeout, + Nodes: utils.SliceToSlice(c.Data.Nodes, func(n *upstream.NodeConfig) string { + return fmt.Sprintf("%s weight=%d", n.Address, n.Weight) + }), + } + } + + return &gateway.ProjectRelease{ + Id: projectID, + Version: version, + Apis: apis, + Upstream: upstreamRelease, + }, nil +} + +func (m *imlPublishModule) getReleaseInfo(ctx context.Context, projectID, releaseId, version string, clusterIds []string) (map[string]*gateway.ProjectRelease, error) { + commits, err := m.releaseService.GetCommits(ctx, releaseId) + if err != nil { + return nil, err + } + apiIds := make([]string, 0, len(commits)) + apiProxyCommitIds := make([]string, 0, len(commits)) + upstreamCommitIds := make([]string, 0, len(commits)) + for _, c := range commits { + switch c.Type { + case release.CommitApiProxy: + apiIds = append(apiIds, c.Target) + apiProxyCommitIds = append(apiProxyCommitIds, c.Commit) + case release.CommitUpstream: + upstreamCommitIds = append(upstreamCommitIds, c.Commit) + } + } + + apiInfos, err := m.apiService.ListInfo(ctx, apiIds...) + if err != nil { + return nil, err + } + + proxyCommits, err := m.apiService.ListProxyCommit(ctx, apiProxyCommitIds...) + if err != nil { + return nil, err + } + proxyCommitMap := utils.SliceToMapO(proxyCommits, func(c *commit.Commit[api.Proxy]) (string, *api.Proxy) { + return c.Target, c.Data + }) + + upstreamCommits, err := m.upstreamService.ListCommit(ctx, upstreamCommitIds...) + if err != nil { + return nil, err + } + apis := make([]*gateway.ApiRelease, 0, len(apiInfos)) + for _, a := range apiInfos { + apiInfo := &gateway.ApiRelease{ + BasicItem: &gateway.BasicItem{ + ID: a.UUID, + Description: a.Description, + Version: version, + }, + Path: a.Path, + Method: []string{a.Method}, + Service: a.Upstream, + } + proxy, ok := proxyCommitMap[a.UUID] + if ok { + apiInfo.Plugins = utils.MapChange(proxy.Plugins, func(v api.PluginSetting) *gateway.Plugin { + return &gateway.Plugin{ + Config: v.Config, + Disable: v.Disable, + } + }) + apiInfo.Extends = proxy.Extends + apiInfo.ProxyPath = proxy.Path + apiInfo.ProxyHeaders = utils.SliceToSlice(proxy.Headers, func(h *api.Header) *gateway.ProxyHeader { + return &gateway.ProxyHeader{ + Key: h.Key, + Value: h.Value, + } + }) + apiInfo.Retry = proxy.Retry + apiInfo.Timeout = proxy.Timeout + } + apis = append(apis, apiInfo) + } + projectReleaseMap := make(map[string]*gateway.ProjectRelease) + upstreamReleaseMap := make(map[string]*gateway.UpstreamRelease) + + for _, c := range upstreamCommits { + for _, partitionId := range clusterIds { + upstreamRelease := &gateway.UpstreamRelease{ + BasicItem: &gateway.BasicItem{ + ID: c.Target, + Version: version, + MatchLabels: map[string]string{ + "serviceId": projectID, + }, + }, + PassHost: c.Data.PassHost, + Scheme: c.Data.Scheme, + Balance: c.Data.Balance, + Timeout: c.Data.Timeout, + Nodes: utils.SliceToSlice(c.Data.Nodes, func(n *upstream.NodeConfig) string { + return fmt.Sprintf("%s weight=%d", n.Address, n.Weight) + }), + } + + upstreamReleaseMap[partitionId] = upstreamRelease + } + } + + for _, clusterId := range clusterIds { + projectReleaseMap[clusterId] = &gateway.ProjectRelease{ + Id: projectID, + Version: version, + Apis: apis, + Upstream: upstreamReleaseMap[clusterId], + } + } + return projectReleaseMap, nil +} + +func (m *imlPublishModule) PublishStatuses(ctx context.Context, serviceId string, id string) ([]*dto.PublishStatus, error) { + _, err := m.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return nil, err + } + flow, err := m.publishService.Get(ctx, id) + if err != nil { + return nil, err + } + if flow.Service != serviceId { + return nil, errors.New("服务不一致") + } + list, err := m.publishService.GetPublishStatus(ctx, id) + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, func(s *publish.Status) *dto.PublishStatus { + status := s.Status + errMsg := s.Error + if s.Status == publish.StatusPublishing && time.Now().Sub(s.UpdateAt) > 30*time.Second { + status = publish.StatusPublishError + errMsg = "发布超时" + } + return &dto.PublishStatus{ + //Cluster: auto.UUID(s.Cluster), + Status: status.String(), + Error: errMsg, + } + + }), nil +} + +// Apply applies the changes to the imlPublishModule. +// +// ctx context.Context, serviceId string, input *dto.ApplyInput +// *dto.Publish, error +func (m *imlPublishModule) Apply(ctx context.Context, serviceId string, input *dto.ApplyInput) (*dto.Publish, error) { + _, err := m.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return nil, err + } + err = m.checkPublish(ctx, serviceId, input.Release) + if err != nil { + return nil, err + } + + previous := "" + running, err := m.releaseService.GetRunning(ctx, serviceId) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + + return nil, err + } + if running != nil { + previous = running.UUID + } + + releaseToPublish, err := m.releaseService.GetRelease(ctx, input.Release) + if err != nil { + // 目标版本不存在 + return nil, err + } + + newPublishId := uuid.NewString() + diff, ok, err := m.projectDiffModule.DiffForLatest(ctx, serviceId, previous) + if err != nil { + return nil, err + } + if !ok { + return nil, errors.New("latest completeness check failed") + } + err = m.publishService.Create(ctx, newPublishId, serviceId, releaseToPublish.UUID, previous, releaseToPublish.Version, input.Remark, diff) + if err != nil { + return nil, err + } + np, err := m.publishService.Get(ctx, newPublishId) + if err != nil { + return nil, err + } + return dto.FromModel(np, releaseToPublish.Remark), nil +} + +func (m *imlPublishModule) CheckPublish(ctx context.Context, serviceId string, releaseId string) (*dto.DiffOut, error) { + _, err := m.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return nil, err + } + err = m.checkPublish(ctx, serviceId, releaseId) + if err != nil { + return nil, err + } + + running, err := m.releaseService.GetRunning(ctx, serviceId) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + runningReleaseId := "" + if running != nil { + runningReleaseId = running.UUID + } + if releaseId == "" { + // 发布latest 版本 + diff, _, err := m.projectDiffModule.DiffForLatest(ctx, serviceId, runningReleaseId) + if err != nil { + return nil, err + } + return m.projectDiffModule.Out(ctx, diff) + } else { + // 发布 releaseId 版本, 返回 与当前版本的差异 + diff, err := m.projectDiffModule.Diff(ctx, serviceId, runningReleaseId, releaseId) + if err != nil { + return nil, err + } + return m.projectDiffModule.Out(ctx, diff) + } + +} +func (m *imlPublishModule) checkPublish(ctx context.Context, serviceId string, releaseId string) error { + flows, err := m.publishService.ListForStatus(ctx, serviceId, publish.StatusApply, publish.StatusAccept) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if len(flows) > 0 { + return errors.New("正在发布中") + } + running, err := m.releaseService.GetRunning(ctx, serviceId) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + + if running == nil { + return nil + } + if running.UUID == releaseId { + return errors.New("不能申请发布当前版本") + } + return nil +} +func (m *imlPublishModule) Close(ctx context.Context, serviceId, id string) error { + err := m.publishService.SetStatus(ctx, serviceId, id, publish.StatusClose) + if err != nil { + return err + } + + return nil +} + +func (m *imlPublishModule) Stop(ctx context.Context, serviceId string, id string) error { + _, err := m.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return err + } + flow, err := m.publishService.Get(ctx, id) + if err != nil { + return err + } + if flow.Service != serviceId { + return errors.New("项目不一致") + } + + if flow.Status != publish.StatusApply && flow.Status != publish.StatusAccept { + return errors.New("只有发布中状态才能停止") + } + status := publish.StatusStop + if flow.Status == publish.StatusApply { + status = publish.StatusClose + } + return m.publishService.SetStatus(ctx, serviceId, id, status) +} + +func (m *imlPublishModule) Refuse(ctx context.Context, serviceId string, id string, commits string) error { + _, err := m.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return err + } + return m.publishService.Refuse(ctx, serviceId, id, commits) +} + +func (m *imlPublishModule) Accept(ctx context.Context, serviceId string, id string, commits string) error { + _, err := m.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return err + } + return m.publishService.Accept(ctx, serviceId, id, commits) +} + +func (m *imlPublishModule) publish(ctx context.Context, id string, clusterId string, projectRelease *gateway.ProjectRelease) error { + + publishStatus := &publish.Status{ + Publish: id, + Status: publish.StatusPublishing, + UpdateAt: time.Now(), + } + err := m.publishService.SetPublishStatus(ctx, publishStatus) + if err != nil { + return fmt.Errorf("set publishing publishStatus error: %v", err) + } + defer func() { + err := m.publishService.SetPublishStatus(ctx, publishStatus) + if err != nil { + log.Errorf("set publishing publishStatus error: %v", err) + } + }() + + client, err := m.clusterService.GatewayClient(ctx, clusterId) + if err != nil { + publishStatus.Status = publish.StatusPublishError + publishStatus.Error = err.Error() + publishStatus.UpdateAt = time.Now() + return fmt.Errorf("get gateway client error: %v", err) + } + defer func() { + err := client.Close(ctx) + if err != nil { + log.Warn("close apinto client:", err) + } + }() + err = client.Project().Online(ctx, projectRelease) + if err != nil { + publishStatus.Status = publish.StatusPublishError + publishStatus.Error = err.Error() + publishStatus.UpdateAt = time.Now() + return fmt.Errorf("online error: %v", err) + } + apiIds := utils.SliceToSlice(projectRelease.Apis, func(api *gateway.ApiRelease) string { + return api.ID + }) + client.Service().Online(ctx, &gateway.ServiceRelease{ + ID: projectRelease.Id, + Apis: apiIds, + }) + publishStatus.Status = publish.StatusDone + publishStatus.UpdateAt = time.Now() + return nil +} + +func (m *imlPublishModule) Publish(ctx context.Context, serviceId string, id string) error { + _, err := m.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return err + } + flow, err := m.publishService.Get(ctx, id) + if err != nil { + return err + } + if flow.Service != serviceId { + return errors.New("服务不一致") + } + if flow.Status != publish.StatusAccept && flow.Status != publish.StatusDone { + return errors.New("只有通过状态才能发布") + } + clusters, err := m.clusterService.List(ctx) + if err != nil { + return err + } + clusterIds := utils.SliceToSlice(clusters, func(i *cluster.Cluster) string { + return i.Uuid + }) + + projectReleaseMap, err := m.getReleaseInfo(ctx, serviceId, flow.Release, flow.Release, clusterIds) + if err != nil { + return err + } + hasError := false + + for _, c := range clusters { + err = m.publish(ctx, flow.Id, c.Uuid, projectReleaseMap[c.Uuid]) + if err != nil { + hasError = true + log.Error(err) + continue + } + } + err = m.releaseService.SetRunning(ctx, serviceId, flow.Release) + if err != nil { + return err + } + status := publish.StatusDone + if hasError { + status = publish.StatusPublishError + } + return m.publishService.SetStatus(ctx, serviceId, id, status) +} + +func (m *imlPublishModule) List(ctx context.Context, serviceId string, page, pageSize int) ([]*dto.Publish, int64, error) { + _, err := m.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return nil, 0, err + } + list, total, err := m.publishService.ListProjectPage(ctx, serviceId, page, pageSize) + if err != nil { + return nil, 0, err + } + + return utils.SliceToSlice(list, func(s *publish.Publish) *dto.Publish { + return dto.FromModel(s, "") + }), total, nil +} + +func (m *imlPublishModule) Detail(ctx context.Context, serviceId string, id string) (*dto.PublishDetail, error) { + _, err := m.serviceService.Check(ctx, serviceId, asServer) + if err != nil { + return nil, err + } + flow, err := m.publishService.Get(ctx, id) + if err != nil { + return nil, err + } + if flow.Service != serviceId { + return nil, errors.New("项目不一致") + } + diff, err := m.publishService.GetDiff(ctx, id) + if err != nil { + return nil, err + } + out, err := m.projectDiffModule.Out(ctx, diff) + if err != nil { + return nil, err + } + publishStatuses, err := m.PublishStatuses(ctx, serviceId, id) + if err != nil { + return nil, err + } + releaseInfo, err := m.releaseService.GetRelease(ctx, flow.Release) + if err != nil { + return nil, err + } + return &dto.PublishDetail{ + Publish: dto.FromModel(flow, releaseInfo.Remark), + Diffs: out, + PublishStatuses: publishStatuses, + }, nil + +} diff --git a/module/publish/module.go b/module/publish/module.go new file mode 100644 index 00000000..94cb3780 --- /dev/null +++ b/module/publish/module.go @@ -0,0 +1,33 @@ +package publish + +import ( + "context" + "reflect" + + "github.com/APIParkLab/APIPark/gateway" + + "github.com/APIParkLab/APIPark/module/publish/dto" + "github.com/eolinker/go-common/autowire" +) + +type IPublishModule interface { + CheckPublish(ctx context.Context, serviceId string, releaseId string) (*dto.DiffOut, error) + //ReleaseDo(ctx context.Context, serviceId string, input *dto.ApplyOnReleaseInput) error + + Apply(ctx context.Context, serviceId string, input *dto.ApplyInput) (*dto.Publish, error) + Stop(ctx context.Context, serviceId string, id string) error + Refuse(ctx context.Context, serviceId string, id string, commits string) error + Accept(ctx context.Context, serviceId string, id string, commits string) error + Publish(ctx context.Context, serviceId string, id string) error + List(ctx context.Context, serviceId string, page, pageSize int) ([]*dto.Publish, int64, error) + Detail(ctx context.Context, serviceId string, id string) (*dto.PublishDetail, error) + PublishStatuses(ctx context.Context, serviceId string, id string) ([]*dto.PublishStatus, error) +} + +func init() { + autowire.Auto[IPublishModule](func() reflect.Value { + m := new(imlPublishModule) + gateway.RegisterInitHandleFunc(m.initGateway) + return reflect.ValueOf(m) + }) +} diff --git a/module/release/dto/input.go b/module/release/dto/input.go new file mode 100644 index 00000000..47d59e65 --- /dev/null +++ b/module/release/dto/input.go @@ -0,0 +1,6 @@ +package dto + +type CreateInput struct { + Version string `json:"version,omitempty"` + Remark string `json:"remark,omitempty"` +} diff --git a/module/release/dto/release.go b/module/release/dto/release.go new file mode 100644 index 00000000..bbc3e195 --- /dev/null +++ b/module/release/dto/release.go @@ -0,0 +1,29 @@ +package dto + +import ( + "github.com/APIParkLab/APIPark/module/publish/dto" + "github.com/eolinker/go-common/auto" +) + +type Release struct { + Id string `json:"id,omitempty"` + Version string `json:"version,omitempty"` + Service auto.Label `json:"service,omitempty" aolabel:"service"` + CreateTime auto.TimeLabel `json:"create_time"` + Creator auto.Label `json:"creator" aolabel:"user"` + Status Status `json:"status,omitempty"` + FlowId string `json:"flowId,omitempty"` + Remark string `json:"remark,omitempty"` + CanDelete bool `json:"can_delete,omitempty"` + CanRollback bool `json:"can_rollback,omitempty"` +} + +type Detail struct { + Id string `json:"id,omitempty"` + Version string `json:"version,omitempty"` + Remark string `json:"remark,omitempty"` + Service auto.Label `json:"service,omitempty" aolabel:"service"` + CreateTime auto.TimeLabel `json:"createTime"` + Creator auto.Label `json:"creator" aolabel:"user"` + Diffs *dto.DiffOut `json:"diffs,omitempty"` +} diff --git a/module/release/dto/status.go b/module/release/dto/status.go new file mode 100644 index 00000000..08c8d80d --- /dev/null +++ b/module/release/dto/status.go @@ -0,0 +1,29 @@ +package dto + +import "encoding/json" + +type Status int + +const ( + StatusNone = iota + StatusRunning + StatusApply + StatusAccept + StatusError + statusMaxValue +) + +var ( + names = []string{"none", "running", "apply", "accept", "error"} +) + +func (s Status) String() string { + if s > 0 && s < statusMaxValue { + return names[s] + } + return names[0] +} + +func (s Status) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} diff --git a/module/release/iml.go b/module/release/iml.go new file mode 100644 index 00000000..3e0333a4 --- /dev/null +++ b/module/release/iml.go @@ -0,0 +1,290 @@ +package release + +import ( + "context" + "errors" + "fmt" + + "github.com/APIParkLab/APIPark/service/cluster" + "github.com/APIParkLab/APIPark/service/service" + "github.com/APIParkLab/APIPark/service/service_diff" + + "github.com/APIParkLab/APIPark/module/release/dto" + serviceDiff "github.com/APIParkLab/APIPark/module/service-diff" + "github.com/APIParkLab/APIPark/service/api" + "github.com/APIParkLab/APIPark/service/publish" + "github.com/APIParkLab/APIPark/service/release" + "github.com/APIParkLab/APIPark/service/universally/commit" + "github.com/APIParkLab/APIPark/service/upstream" + "github.com/eolinker/go-common/auto" + "github.com/eolinker/go-common/store" + "github.com/eolinker/go-common/utils" + "gorm.io/gorm" +) + +var ( + _ IReleaseModule = (*imlReleaseModule)(nil) + projectRuleMustServer = map[string]bool{ + "as_server": true, + } +) + +type imlReleaseModule struct { + projectDiffModule serviceDiff.IServiceDiffModule `autowired:""` + releaseService release.IReleaseService `autowired:""` + apiService api.IAPIService `autowired:""` + upstreamService upstream.IUpstreamService `autowired:""` + publishService publish.IPublishService `autowired:""` + transaction store.ITransaction `autowired:""` + projectService service.IServiceService `autowired:""` + clusterService cluster.IClusterService `autowired:""` +} + +func (m *imlReleaseModule) Create(ctx context.Context, serviceId string, input *dto.CreateInput) (string, error) { + + proInfo, err := m.projectService.Check(ctx, serviceId, projectRuleMustServer) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", errors.New("project not found") + } + return "", err + } + clusters, err := m.clusterService.List(ctx) + if err != nil || len(clusters) == 0 { + return "", fmt.Errorf("cluster not set:%w", err) + } + + apis, err := m.apiService.ListForService(ctx, proInfo.Id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", errors.New("api not found") + } + return "", err + } + if len(apis) == 0 { + return "", errors.New("api not found") + } + apiUUIDS := utils.SliceToSlice(apis, func(a *api.API) string { + return a.UUID + }) + apiProxy, err := m.apiService.ListLatestCommitProxy(ctx, apiUUIDS...) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", errors.New("api config or document not found") + } + return "", err + } + if len(apis) != len(apiProxy) { + return "", errors.New("api or document not found") + } + apiDocs, err := m.apiService.ListLatestCommitDocument(ctx, apiUUIDS...) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", errors.New("api config or document not found") + } + return "", err + } + if len(apis) != len(apiDocs) { + return "", errors.New("api or document not found") + } + upstreams, err := m.upstreamService.ListLatestCommit(ctx, serviceId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", errors.New("api config or document not found") + } + return "", err + } + + apiProxyCommits := utils.SliceToMapO(apiProxy, func(c *commit.Commit[api.Proxy]) (string, string) { + return c.Target, c.UUID + }) + apiDocumentCommits := utils.SliceToMapO(apiDocs, func(c *commit.Commit[api.Document]) (string, string) { + return c.Target, c.UUID + }) + upstreamCommits := utils.SliceToMapArray(upstreams, func(c *commit.Commit[upstream.Config]) string { + return c.Target + }) + upstreamCommitsForUKC := utils.MapChange(upstreamCommits, func(ls []*commit.Commit[upstream.Config]) map[string]string { + return utils.SliceToMapO(ls, func(c *commit.Commit[upstream.Config]) (string, string) { + return c.Key, c.UUID + }) + }) + if !m.releaseService.Completeness(utils.SliceToSlice(clusters, func(s *cluster.Cluster) string { + return s.Uuid + }), apiUUIDS, apiProxy, apiDocs, upstreams) { + return "", errors.New("completeness check failed") + } + newRelease, err := m.releaseService.CreateRelease(ctx, serviceId, input.Version, input.Remark, apiProxyCommits, apiDocumentCommits, upstreamCommitsForUKC) + if err != nil { + return "", err + } + return newRelease.UUID, err +} + +func (m *imlReleaseModule) Detail(ctx context.Context, project string, id string) (*dto.Detail, error) { + r, err := m.releaseService.GetRelease(ctx, id) + if err != nil { + return nil, err + } + if r.Service != project { + return nil, errors.New("release not found") + } + running, err := m.releaseService.GetRunning(ctx, project) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + runningRelease := "" + if running != nil { + runningRelease = running.UUID + } + diff, err := m.projectDiffModule.Diff(ctx, project, runningRelease, r.UUID) + if err != nil { + return nil, err + } + out, err := m.projectDiffModule.Out(ctx, diff) + if err != nil { + return nil, err + } + return &dto.Detail{ + Id: r.UUID, + Version: r.Version, + Remark: r.Remark, + Service: auto.UUID(r.Service), + CreateTime: auto.TimeLabel(r.CreateAt), + Creator: auto.UUID(r.Creator), + Diffs: out, + }, nil +} + +func (m *imlReleaseModule) List(ctx context.Context, project string) ([]*dto.Release, error) { + _, err := m.projectService.Check(ctx, project, projectRuleMustServer) + if err != nil { + return nil, err + } + list, err := m.releaseService.List(ctx, project) + if err != nil { + return nil, err + } + running, err := m.releaseService.GetRunning(ctx, project) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + + releaseIds := utils.SliceToSlice(list, func(s *release.Release) string { + return s.UUID + }) + flows, err := m.publishService.Latest(ctx, releaseIds...) + if err != nil { + return nil, err + } + flowMap := utils.SliceToMap(flows, func(s *publish.Publish) string { + return s.Release + }) + + return utils.SliceToSlice(list, func(s *release.Release) *dto.Release { + + r := &dto.Release{ + Id: s.UUID, + Service: auto.UUID(s.Service), + Version: s.Version, + Remark: s.Remark, + Status: dto.StatusNone, + CanRollback: false, + CanDelete: true, + Creator: auto.UUID(s.Creator), + CreateTime: auto.TimeLabel(s.CreateAt), + } + + if running != nil && running.UUID == s.UUID { + r.Status = dto.StatusRunning + r.CanRollback = true + r.CanDelete = false + } + flow, has := flowMap[s.UUID] + if has { + r.FlowId = flow.Id + + if flow.Status == publish.StatusApply { + r.Status = dto.StatusApply + r.CanDelete = false + } else if flow.Status == publish.StatusAccept { + + r.Status = dto.StatusAccept + r.CanDelete = false + } else if flow.Status == publish.StatusPublishError { + r.Status = dto.StatusError + r.CanDelete = false + } + } + return r + }), nil +} + +func (m *imlReleaseModule) Delete(ctx context.Context, project string, id string) error { + _, err := m.projectService.Check(ctx, project, projectRuleMustServer) + if err != nil { + return err + } + return m.transaction.Transaction(ctx, func(ctx context.Context) error { + r, err := m.releaseService.GetRelease(ctx, id) + if err != nil { + return err + } + if r == nil { + return errors.New("release not found") + } + if r.Service != project { + return errors.New("project not match") + } + running, err := m.releaseService.GetRunning(ctx, project) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if running != nil && running.UUID == id { + return errors.New("can not delete running release") + } + flow, err := m.publishService.GetLatest(ctx, id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if flow != nil { + if flow.Status == publish.StatusApply || flow.Status == publish.StatusAccept { + return errors.New("can not delete release in apply or approve flow") + } + } + return m.releaseService.DeleteRelease(ctx, id) + }) + +} + +func (m *imlReleaseModule) Preview(ctx context.Context, project string) (*dto.Release, *service_diff.Diff, bool, error) { + _, err := m.projectService.Check(ctx, project, projectRuleMustServer) + if err != nil { + return nil, nil, false, err + } + running, err := m.releaseService.GetRunning(ctx, project) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil, false, err + } + + if running == nil { + running = new(release.Release) + } + + diff, completeness, err := m.projectDiffModule.DiffForLatest(ctx, project, running.UUID) + if err != nil { + return nil, nil, false, err + } + return &dto.Release{ + Id: running.UUID, + Version: running.Version, + Service: auto.UUID(project), + CreateTime: auto.TimeLabel(running.CreateAt), + Creator: auto.UUID(running.Creator), + Status: dto.StatusNone, + Remark: running.Remark, + CanDelete: false, + CanRollback: false, + }, diff, completeness, nil + +} diff --git a/module/release/module.go b/module/release/module.go new file mode 100644 index 00000000..69c55601 --- /dev/null +++ b/module/release/module.go @@ -0,0 +1,24 @@ +package release + +import ( + "context" + "reflect" + + "github.com/APIParkLab/APIPark/module/release/dto" + "github.com/APIParkLab/APIPark/service/service_diff" + "github.com/eolinker/go-common/autowire" +) + +type IReleaseModule interface { + Create(ctx context.Context, service string, input *dto.CreateInput) (string, error) + Detail(ctx context.Context, service string, id string) (*dto.Detail, error) + List(ctx context.Context, service string) ([]*dto.Release, error) + Delete(ctx context.Context, service string, id string) error + Preview(ctx context.Context, service string) (*dto.Release, *service_diff.Diff, bool, error) +} + +func init() { + autowire.Auto[IReleaseModule](func() reflect.Value { + return reflect.ValueOf(new(imlReleaseModule)) + }) +} diff --git a/module/service-diff/diff.go b/module/service-diff/diff.go new file mode 100644 index 00000000..80e40bc8 --- /dev/null +++ b/module/service-diff/diff.go @@ -0,0 +1,25 @@ +package service_diff + +import ( + "context" + "reflect" + + "github.com/APIParkLab/APIPark/service/service_diff" + "github.com/eolinker/go-common/autowire" +) + +var ( + _ IServiceDiffModule = (*imlServiceDiff)(nil) +) + +type IServiceDiffModule interface { + Diff(ctx context.Context, serviceId string, baseRelease, targetRelease string) (*service_diff.Diff, error) + DiffForLatest(ctx context.Context, serviceId string, baseRelease string) (*service_diff.Diff, bool, error) + Out(ctx context.Context, diff *service_diff.Diff) (*DiffOut, error) +} + +func init() { + autowire.Auto[IServiceDiffModule](func() reflect.Value { + return reflect.ValueOf(new(imlServiceDiff)) + }) +} diff --git a/module/service-diff/iml.go b/module/service-diff/iml.go new file mode 100644 index 00000000..857d5524 --- /dev/null +++ b/module/service-diff/iml.go @@ -0,0 +1,330 @@ +package service_diff + +import ( + "context" + "errors" + "fmt" + + "github.com/APIParkLab/APIPark/service/api" + "github.com/APIParkLab/APIPark/service/cluster" + "github.com/APIParkLab/APIPark/service/release" + "github.com/APIParkLab/APIPark/service/service_diff" + "github.com/APIParkLab/APIPark/service/universally/commit" + "github.com/APIParkLab/APIPark/service/upstream" + "github.com/eolinker/go-common/auto" + "github.com/eolinker/go-common/utils" +) + +type imlServiceDiff struct { + apiService api.IAPIService `autowired:""` + upstreamService upstream.IUpstreamService `autowired:""` + releaseService release.IReleaseService `autowired:""` + clusterService cluster.IClusterService `autowired:""` +} + +func (m *imlServiceDiff) Diff(ctx context.Context, serviceId string, baseRelease, targetRelease string) (*service_diff.Diff, error) { + if targetRelease == "" { + return nil, fmt.Errorf("target release is required") + } + + var target *projectInfo + + targetReleaseValue, err := m.releaseService.GetRelease(ctx, targetRelease) + if err != nil { + return nil, fmt.Errorf("get target release failed:%w", err) + } + if targetReleaseValue.Service != serviceId { + return nil, errors.New("project not match") + } + + target, err = m.getReleaseInfo(ctx, targetRelease) + if err != nil { + return nil, err + } + base, err := m.getBaseInfo(ctx, serviceId, baseRelease) + if err != nil { + return nil, err + } + target.id = serviceId + clusters, err := m.clusterService.List(ctx) + if err != nil { + return nil, err + } + clusterIds := utils.SliceToSlice(clusters, func(i *cluster.Cluster) string { + return i.Uuid + }) + diff := m.diff(clusterIds, base, target) + return diff, nil + +} +func (m *imlServiceDiff) getBaseInfo(ctx context.Context, serviceId, baseRelease string) (*projectInfo, error) { + if baseRelease == "" { + return &projectInfo{}, nil + } + baseReleaseValue, err := m.releaseService.GetRelease(ctx, baseRelease) + if err != nil { + return nil, fmt.Errorf("get base release failed:%w", err) + } + if baseReleaseValue.Service != serviceId { + return nil, errors.New("project not match") + } + base, err := m.getReleaseInfo(ctx, baseRelease) + if err != nil { + return nil, fmt.Errorf("get base release info failed:%w", err) + } + + return base, nil +} +func (m *imlServiceDiff) DiffForLatest(ctx context.Context, serviceId string, baseRelease string) (*service_diff.Diff, bool, error) { + + apis, err := m.apiService.ListForService(ctx, serviceId) + if err != nil { + return nil, false, err + } + + apiIds := utils.SliceToSlice(apis, func(i *api.API) string { + return i.UUID + }) + apiInfos, err := m.apiService.ListInfo(ctx, apiIds...) + if err != nil { + return nil, false, err + } + proxy, err := m.apiService.ListLatestCommitProxy(ctx, apiIds...) + if err != nil { + return nil, false, fmt.Errorf("diff for api commit %v", err) + } + documents, err := m.apiService.ListLatestCommitDocument(ctx, apiIds...) + if err != nil { + return nil, false, err + } + + upstreamCommits, err := m.upstreamService.ListLatestCommit(ctx, serviceId) + if err != nil { + return nil, false, err + } + + base, err := m.getBaseInfo(ctx, serviceId, baseRelease) + if err != nil { + return nil, false, err + } + target := &projectInfo{ + id: serviceId, + apis: apiInfos, + apiCommits: proxy, + apiDocs: documents, + upstreamCommits: upstreamCommits, + } + clusters, err := m.clusterService.List(ctx) + if err != nil { + return nil, false, err + } + clusterIds := utils.SliceToSlice(clusters, func(i *cluster.Cluster) string { + return i.Uuid + }) + return m.diff(clusterIds, base, target), true, nil +} +func (m *imlServiceDiff) getReleaseInfo(ctx context.Context, releaseId string) (*projectInfo, error) { + commits, err := m.releaseService.GetCommits(ctx, releaseId) + if err != nil { + return nil, err + } + + apiIds := utils.SliceToSlice(commits, func(i *release.ProjectCommits) string { + return i.Target + }, func(c *release.ProjectCommits) bool { + return c.Type == release.CommitApiProxy || c.Type == release.CommitApiDocument + }) + apiInfos, err := m.apiService.ListInfo(ctx, apiIds...) + if err != nil { + return nil, err + } + apiProxyCommitIds := utils.SliceToSlice(commits, func(i *release.ProjectCommits) string { + return i.Commit + }, func(c *release.ProjectCommits) bool { + return c.Type == release.CommitApiProxy + }) + apiDocumentCommitIds := utils.SliceToSlice(commits, func(i *release.ProjectCommits) string { + return i.Commit + }, func(c *release.ProjectCommits) bool { + return c.Type == release.CommitApiDocument + }) + upstreamCommitIds := utils.SliceToSlice(commits, func(i *release.ProjectCommits) string { + return i.Commit + }, func(c *release.ProjectCommits) bool { + return c.Type == release.CommitUpstream + }) + proxyCommits, err := m.apiService.ListProxyCommit(ctx, apiProxyCommitIds...) + if err != nil { + return nil, err + } + documentCommits, err := m.apiService.ListDocumentCommit(ctx, apiDocumentCommitIds...) + if err != nil { + return nil, err + } + upstreamCommits, err := m.upstreamService.ListCommit(ctx, upstreamCommitIds...) + if err != nil { + return nil, err + } + return &projectInfo{ + apis: apiInfos, + apiCommits: proxyCommits, + apiDocs: documentCommits, + upstreamCommits: upstreamCommits, + }, nil +} +func (m *imlServiceDiff) diff(partitions []string, base, target *projectInfo) *service_diff.Diff { + out := &service_diff.Diff{ + Apis: nil, + Upstreams: nil, + //Clusters: partitions, + } + baseApis := utils.NewSet(utils.SliceToSlice(base.apis, func(i *api.Info) string { + return i.UUID + })...) + baseApiProxy := utils.SliceToMap(base.apiCommits, func(i *commit.Commit[api.Proxy]) string { + return i.Target + }) + baseAPIDoc := utils.SliceToMap(base.apiDocs, func(i *commit.Commit[api.Document]) string { + return i.Target + }) + + targetApiProxy := utils.SliceToMap(target.apiCommits, func(i *commit.Commit[api.Proxy]) string { + return i.Target + }) + targetAPIDoc := utils.SliceToMap(target.apiDocs, func(i *commit.Commit[api.Document]) string { + return i.Target + }) + + for _, apiInfo := range target.apis { + apiId := apiInfo.UUID + a := &service_diff.ApiDiff{ + APi: apiInfo.UUID, + Name: apiInfo.Name, + Method: apiInfo.Method, + Path: apiInfo.Path, + Status: service_diff.Status{}, + } + + pc, hasPc := targetApiProxy[apiId] + dc, hasDC := targetAPIDoc[apiId] + if !hasPc { + // 未设置proxy信息 + a.Status.Proxy = service_diff.StatusUnset + } + if !hasDC { + // 未设置文档 + a.Status.Doc = service_diff.StatusUnset + } + + if !baseApis.Has(apiId) { + a.Change = service_diff.ChangeTypeNew + } else { + a.Change = service_diff.ChangeTypeNone + + baseProxy, hasBaseProxy := baseApiProxy[apiId] + baseDoc, hasBaseDoc := baseAPIDoc[apiId] + if hasBaseDoc != hasDC || hasBaseProxy != hasPc { + // 文档或者proxy变更 + a.Change = service_diff.ChangeTypeUpdate + } else if (hasPc && pc.UUID != baseProxy.UUID) || (hasDC && dc.UUID != baseDoc.UUID) { + // 文档 或者 proxy 变更 + a.Change = service_diff.ChangeTypeUpdate + } + } + out.Apis = append(out.Apis, a) + + } + baseApis.Remove(utils.SliceToSlice(out.Apis, func(i *service_diff.ApiDiff) string { + return i.APi + })...) + for _, apiInfo := range base.apis { + if baseApis.Has(apiInfo.UUID) { + out.Apis = append(out.Apis, &service_diff.ApiDiff{ + APi: apiInfo.UUID, + Name: apiInfo.Name, + Method: apiInfo.Method, + Path: apiInfo.Path, + Status: service_diff.Status{}, + Change: service_diff.ChangeTypeDelete, + }) + } + + } + // upstream diff + targetUpstreamMap := utils.SliceToMap(target.upstreamCommits, func(i *commit.Commit[upstream.Config]) string { + return fmt.Sprintf("%s-%s", i.Target, i.Key) + }) + baseUpstreamMap := utils.SliceToMap(base.upstreamCommits, func(i *commit.Commit[upstream.Config]) string { + return fmt.Sprintf("%s-%s", i.Target, i.Key) + }) + + for _, partitionId := range partitions { + key := fmt.Sprintf("%s-%s", target.id, partitionId) + o := &service_diff.UpstreamDiff{ + Upstream: target.id, + //Partition: partitionId, + Data: nil, + Change: service_diff.ChangeTypeNone, + Status: 0, + } + out.Upstreams = append(out.Upstreams, o) + bu, hasBu := baseUpstreamMap[key] + tu, hasTu := targetUpstreamMap[key] + if hasTu { + o.Data = tu.Data + if !hasBu { + o.Change = service_diff.ChangeTypeNew + } else if tu.UUID != bu.UUID { + o.Change = service_diff.ChangeTypeUpdate + } + + } else { + o.Status = service_diff.StatusLoss + if hasBu { + o.Change = service_diff.ChangeTypeDelete + } + } + } + + return out +} + +func (m *imlServiceDiff) Out(ctx context.Context, diff *service_diff.Diff) (*DiffOut, error) { + + clusters, err := m.clusterService.List(ctx, diff.Clusters...) + if err != nil { + return nil, err + } + if len(clusters) == 0 { + return nil, fmt.Errorf("unset gateway for clusters %v", diff.Clusters) + } + + out := &DiffOut{} + out.Apis = utils.SliceToSlice(diff.Apis, func(i *service_diff.ApiDiff) *ApiDiffOut { + return &ApiDiffOut{ + Api: auto.UUID(i.APi), + Name: i.Name, + Method: i.Method, + Path: i.Path, + Change: i.Change, + Status: i.Status, + } + }) + + for _, u := range diff.Upstreams { + typeValue := u.Data.Type + + if typeValue == "" { + typeValue = "static" + } + out.Upstreams = append(out.Upstreams, &UpstreamDiffOut{ + Change: u.Change, + Type: typeValue, + Status: u.Status, + Addr: utils.SliceToSlice(u.Data.Nodes, func(i *upstream.NodeConfig) string { + return i.Address + }), + }) + } + return out, nil +} diff --git a/module/service-diff/out.go b/module/service-diff/out.go new file mode 100644 index 00000000..1c204c4e --- /dev/null +++ b/module/service-diff/out.go @@ -0,0 +1,66 @@ +package service_diff + +import ( + "github.com/APIParkLab/APIPark/service/api" + "github.com/APIParkLab/APIPark/service/service_diff" + "github.com/APIParkLab/APIPark/service/universally/commit" + "github.com/APIParkLab/APIPark/service/upstream" + "github.com/eolinker/go-common/auto" +) + +type DiffOut struct { + Apis []*ApiDiffOut `json:"apis"` + Upstreams []*UpstreamDiffOut `json:"upstreams"` +} + +type ApiDiffOut struct { + Api auto.Label `json:"api,omitempty" aolabel:"api"` + Name string `json:"name,omitempty"` + Method string `json:"method,omitempty"` + Path string `json:"path,omitempty"` + //Upstream auto.Label `json:"upstream,omitempty" aolabel:"upstream"` + Change service_diff.ChangeType `json:"change,omitempty"` + Status service_diff.Status `json:"status,omitempty"` +} +type UpstreamDiffOut struct { + Change service_diff.ChangeType `json:"change,omitempty"` + Status service_diff.StatusType `json:"status,omitempty"` + Type string `json:"type,omitempty"` + Addr []string `json:"addr,omitempty"` +} + +// +//func CreateOut(d *project_diff.Diff) *DiffOut { +// if d == nil { +// return nil +// } +// return &DiffOut{ +// Apis: utils.SliceToSlice(d.Apis, func(s *project_diff.ApiDiff) *ApiDiffOut { +// return &ApiDiffOut{ +// Name: s.Name, +// Method: s.Method, +// Path: s.Path, +// Upstream: s.Upstream, +// Change: s.Change, +// } +// }), +// Upstreams: utils.SliceToSlice(d.Upstreams, func(s *project_diff.UpstreamDiff) *UpstreamDiffOut { +// return &UpstreamDiffOut{ +// Upstream: s.Name, +// Cluster: auto.UUID(s.Cluster), +// Cluster: auto.UUID(s.Cluster), +// Change: s.Change, +// Type: s.Type, +// Addr: s.Addr, +// } +// }), +// } +//} + +type projectInfo struct { + id string + apis []*api.Info + apiCommits []*commit.Commit[api.Proxy] + apiDocs []*commit.Commit[api.Document] + upstreamCommits []*commit.Commit[upstream.Config] +} diff --git a/module/service/dto/input.go b/module/service/dto/input.go new file mode 100644 index 00000000..5f49366b --- /dev/null +++ b/module/service/dto/input.go @@ -0,0 +1,50 @@ +package service_dto + +type CreateService struct { + Id string `json:"id"` + Name string `json:"name"` + Prefix string `json:"prefix"` + Description string `json:"description"` + ServiceType string `json:"service_type"` + Logo string `json:"logo"` + Tags []string `json:"tags"` + Catalogue string `json:"catalogue" aocheck:"catalogue"` + AsApp *bool `json:"as_app"` + AsServer *bool `json:"as_server"` +} + +type EditService struct { + Name *string `json:"name"` + Description *string `json:"description"` + ServiceType *string `json:"service_type"` + Catalogue *string `json:"catalogue" aocheck:"catalogue"` + Logo *string `json:"logo"` + Tags *[]string `json:"tags"` +} + +type CreateApp struct { + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` +} + +type UpdateApp struct { + Name *string `json:"name"` + Description *string `json:"description"` +} + +type EditMemberRole struct { + Roles []string `json:"roles"` +} + +type Users struct { + Users []string `json:"users" aocheck:"user"` +} + +type EditProjectMember struct { + Roles []string `json:"roles" aocheck:"role"` +} + +type SaveServiceDoc struct { + Doc string `json:"doc"` +} diff --git a/module/service/dto/output.go b/module/service/dto/output.go new file mode 100644 index 00000000..da0f248e --- /dev/null +++ b/module/service/dto/output.go @@ -0,0 +1,115 @@ +package service_dto + +import ( + "github.com/APIParkLab/APIPark/service/service" + "github.com/eolinker/go-common/auto" +) + +type ServiceItem struct { + Id string `json:"id"` + Name string `json:"name"` + Team auto.Label `json:"team" aolabel:"team"` + ApiNum int64 `json:"api_num"` + Description string `json:"description"` + CreateTime auto.TimeLabel `json:"create_time"` + UpdateTime auto.TimeLabel `json:"update_time"` + CanDelete bool `json:"can_delete"` +} + +type AppItem struct { + Id string `json:"id"` + Name string `json:"name"` + Team auto.Label `json:"team" aolabel:"team"` + SubscribeNum int64 `json:"subscribe_num"` + SubscribeVerifyNum int64 `json:"subscribe_verify_num"` + Description string `json:"description"` + CreateTime auto.TimeLabel `json:"create_time"` + UpdateTime auto.TimeLabel `json:"update_time"` + CanDelete bool `json:"can_delete"` +} + +type SimpleServiceItem struct { + Id string `json:"id"` + Name string `json:"name"` + Team auto.Label `json:"team" aolabel:"team"` + Description string `json:"description"` +} + +type SimpleAppItem struct { + Id string `json:"id"` + Name string `json:"name"` + Team auto.Label `json:"team" aolabel:"team"` + Description string `json:"description"` +} + +type Service struct { + Id string `json:"id"` + Name string `json:"name"` + Prefix string `json:"prefix,omitempty"` + Description string `json:"description"` + Team auto.Label `json:"team" aolabel:"team"` + CreateTime auto.TimeLabel `json:"create_time"` + UpdateTime auto.TimeLabel `json:"update_time"` + ServiceType string `json:"service_type"` + Catalogue auto.Label `json:"catalogue" aolabel:"catalogue"` + Tags []auto.Label `json:"tags" aolabel:"tag"` + Logo string `json:"logo"` + AsServer bool `json:"as_server"` + AsApp bool `json:"as_app"` +} + +type App struct { + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Team auto.Label `json:"team" aolabel:"team"` + CreateTime auto.TimeLabel `json:"create_time"` + UpdateTime auto.TimeLabel `json:"update_time"` + AsApp bool `json:"as_app"` +} + +func ToService(model *service.Service) *Service { + return &Service{ + Id: model.Id, + Name: model.Name, + Prefix: model.Prefix, + Description: model.Description, + Team: auto.UUID(model.Team), + ServiceType: model.ServiceType.String(), + Logo: model.Logo, + Catalogue: auto.UUID(model.Catalogue), + CreateTime: auto.TimeLabel(model.CreateTime), + UpdateTime: auto.TimeLabel(model.UpdateTime), + AsServer: model.AsServer, + AsApp: model.AsApp, + } +} + +type MemberItem struct { + User auto.Label `json:"user" aolabel:"user"` + Email string `json:"email"` + Roles []auto.Label `json:"roles" aolabel:"role"` + CanDelete bool `json:"can_delete"` +} + +type SimpleMemberItem struct { + Id string `json:"id"` + Name string `json:"name"` +} + +type TeamMemberToAdd struct { + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Email string `json:"email,omitempty"` + Department auto.Label `json:"department" aolabel:"department"` +} + +type ServiceDoc struct { + Id string `json:"id"` + Name string `json:"name"` + Doc string `json:"doc"` + Creator auto.Label `json:"creator" aolabel:"user"` + CreateTime auto.TimeLabel `json:"create_time"` + Updater auto.Label `json:"updater" aolabel:"user"` + UpdateTime auto.TimeLabel `json:"update_time"` +} diff --git a/module/service/iml.go b/module/service/iml.go new file mode 100644 index 00000000..313ff39e --- /dev/null +++ b/module/service/iml.go @@ -0,0 +1,727 @@ +package service + +import ( + "context" + "errors" + "fmt" + "sort" + "strings" + + service_tag "github.com/APIParkLab/APIPark/service/service-tag" + + service_doc "github.com/APIParkLab/APIPark/service/service-doc" + + serviceDto "github.com/APIParkLab/APIPark/module/service/dto" + + "github.com/APIParkLab/APIPark/service/tag" + + "github.com/APIParkLab/APIPark/service/service" + + "github.com/APIParkLab/APIPark/service/subscribe" + "gorm.io/gorm" + + "github.com/APIParkLab/APIPark/service/api" + + "github.com/eolinker/go-common/auto" + + team_member "github.com/APIParkLab/APIPark/service/team-member" + + "github.com/eolinker/go-common/store" + + "github.com/google/uuid" + + "github.com/eolinker/go-common/utils" + + "github.com/APIParkLab/APIPark/service/team" + + service_dto "github.com/APIParkLab/APIPark/module/service/dto" +) + +var ( + _ IServiceModule = (*imlServiceModule)(nil) +) + +type imlServiceModule struct { + serviceService service.IServiceService `autowired:""` + teamService team.ITeamService `autowired:""` + teamMemberService team_member.ITeamMemberService `autowired:""` + tagService tag.ITagService `autowired:""` + serviceDocService service_doc.IDocService `autowired:""` + serviceTagService service_tag.ITagService `autowired:""` + apiService api.IAPIService `autowired:""` + transaction store.ITransaction `autowired:""` +} + +func (i *imlServiceModule) searchMyServices(ctx context.Context, teamId string, keyword string) ([]*service.Service, error) { + + userID := utils.UserId(ctx) + condition := make(map[string]interface{}) + condition["as_server"] = true + if teamId != "" { + _, err := i.teamService.Get(ctx, teamId) + if err != nil { + return nil, err + } + condition["team"] = teamId + return i.serviceService.Search(ctx, keyword, condition, "update_at desc") + } else { + membersForUser, err := i.teamMemberService.FilterMembersForUser(ctx, userID) + if err != nil { + return nil, err + } + teamIds := membersForUser[userID] + condition["team"] = teamIds + return i.serviceService.Search(ctx, keyword, condition, "update_at desc") + } + +} + +func (i *imlServiceModule) SearchMyServices(ctx context.Context, teamId string, keyword string) ([]*service_dto.ServiceItem, error) { + services, err := i.searchMyServices(ctx, teamId, keyword) + if err != nil { + return nil, err + } + serviceIds := utils.SliceToSlice(services, func(p *service.Service) string { + return p.Id + }) + apiCountMap, err := i.apiService.CountByGroup(ctx, "", map[string]interface{}{"service": serviceIds}, "service") + if err != nil { + return nil, err + } + + items := make([]*service_dto.ServiceItem, 0, len(services)) + for _, model := range services { + if teamId != "" && model.Team != teamId { + continue + } + apiCount := apiCountMap[model.Id] + items = append(items, &service_dto.ServiceItem{ + Id: model.Id, + Name: model.Name, + Description: model.Description, + CreateTime: auto.TimeLabel(model.CreateTime), + UpdateTime: auto.TimeLabel(model.UpdateTime), + Team: auto.UUID(model.Team), + ApiNum: apiCount, + CanDelete: apiCount == 0, + }) + } + return items, nil +} + +func (i *imlServiceModule) SimpleAPPS(ctx context.Context, keyword string) ([]*service_dto.SimpleServiceItem, error) { + w := make(map[string]interface{}) + w["as_app"] = true + services, err := i.serviceService.Search(ctx, keyword, w) + if err != nil { + return nil, err + } + return utils.SliceToSlice(services, func(p *service.Service) *service_dto.SimpleServiceItem { + return &service_dto.SimpleServiceItem{ + Id: p.Id, + Name: p.Name, + Description: p.Description, + + Team: auto.UUID(p.Team), + } + }), nil +} + +func (i *imlServiceModule) Simple(ctx context.Context, keyword string) ([]*service_dto.SimpleServiceItem, error) { + w := make(map[string]interface{}) + w["as_server"] = true + + services, err := i.serviceService.Search(ctx, keyword, w) + if err != nil { + return nil, err + } + + items := make([]*service_dto.SimpleServiceItem, 0, len(services)) + for _, p := range services { + + items = append(items, &service_dto.SimpleServiceItem{ + Id: p.Id, + Name: p.Name, + Description: p.Description, + Team: auto.UUID(p.Team), + }) + } + return items, nil +} + +func (i *imlServiceModule) MySimple(ctx context.Context, keyword string) ([]*service_dto.SimpleServiceItem, error) { + services, err := i.searchMyServices(ctx, "", keyword) + + if err != nil { + return nil, err + } + + items := make([]*service_dto.SimpleServiceItem, 0, len(services)) + for _, p := range services { + + items = append(items, &service_dto.SimpleServiceItem{ + Id: p.Id, + Name: p.Name, + Description: p.Description, + Team: auto.UUID(p.Team), + }) + } + return items, nil +} + +func (i *imlServiceModule) Get(ctx context.Context, id string) (*service_dto.Service, error) { + serviceInfo, err := i.serviceService.Get(ctx, id) + if err != nil { + return nil, err + } + tags, err := i.serviceTagService.List(ctx, []string{serviceInfo.Id}, nil) + if err != nil { + return nil, err + } + + s := service_dto.ToService(serviceInfo) + s.Tags = auto.List(utils.SliceToSlice(tags, func(p *service_tag.Tag) string { + return p.Tid + })) + return s, nil +} + +func (i *imlServiceModule) Search(ctx context.Context, teamID string, keyword string) ([]*service_dto.ServiceItem, error) { + var list []*service.Service + var err error + if teamID != "" { + _, err = i.teamService.Get(ctx, teamID) + if err != nil { + return nil, err + } + list, err = i.serviceService.Search(ctx, keyword, map[string]interface{}{"team": teamID, "as_server": true}, "update_at desc") + } else { + list, err = i.serviceService.Search(ctx, keyword, map[string]interface{}{"as_server": true}, "update_at desc") + } + if err != nil { + return nil, err + } + + serviceIds := utils.SliceToSlice(list, func(s *service.Service) string { + return s.Id + }) + + apiCountMap, err := i.apiService.CountByGroup(ctx, "", map[string]interface{}{"service": serviceIds}, "service") + if err != nil { + return nil, err + } + //serviceCountMap, err := i.serviceService.CountByGroup(ctx, "", map[string]interface{}{"uuid": serviceIds}, "service") + //if err != nil { + // return nil, err + //} + + items := make([]*service_dto.ServiceItem, 0, len(list)) + for _, model := range list { + apiCount := apiCountMap[model.Id] + //serviceCount := serviceCountMap[model.Id] + items = append(items, &service_dto.ServiceItem{ + Id: model.Id, + Name: model.Name, + Description: model.Description, + CreateTime: auto.TimeLabel(model.CreateTime), + UpdateTime: auto.TimeLabel(model.UpdateTime), + Team: auto.UUID(model.Team), + ApiNum: apiCount, + CanDelete: apiCount == 0, + }) + } + return items, nil +} + +func (i *imlServiceModule) Create(ctx context.Context, teamID string, input *service_dto.CreateService) (*service_dto.Service, error) { + + if input.Id == "" { + input.Id = uuid.New().String() + } + mo := &service.Create{ + Id: input.Id, + Name: input.Name, + Description: input.Description, + Team: teamID, + ServiceType: service.ServiceType(input.ServiceType), + Catalogue: input.Catalogue, + Prefix: input.Prefix, + Logo: input.Logo, + } + if mo.ServiceType == service.PublicService && mo.Catalogue == "" { + return nil, fmt.Errorf("catalogue can not be empty") + } + if input.AsApp == nil { + // 默认值为false + mo.AsApp = false + } else { + mo.AsApp = *input.AsApp + } + if input.AsServer == nil { + // 默认值为true + mo.AsServer = true + } else { + mo.AsServer = *input.AsServer + } + input.Prefix = strings.Trim(strings.Trim(input.Prefix, " "), "/") + err := i.transaction.Transaction(ctx, func(ctx context.Context) error { + if input.Tags != nil { + tags, err := i.getTagUuids(ctx, input.Tags) + if err != nil { + return err + } + for _, t := range tags { + err = i.serviceTagService.Create(ctx, &service_tag.CreateTag{ + Tid: t, + Sid: input.Id, + }) + if err != nil { + return err + } + } + } + return i.serviceService.Create(ctx, mo) + }) + if err != nil { + return nil, err + } + return i.Get(ctx, input.Id) +} + +func (i *imlServiceModule) Edit(ctx context.Context, id string, input *service_dto.EditService) (*service_dto.Service, error) { + _, err := i.serviceService.Get(ctx, id) + if err != nil { + return nil, err + } + err = i.transaction.Transaction(ctx, func(ctx context.Context) error { + serviceType := (*service.ServiceType)(input.ServiceType) + if serviceType != nil && *serviceType == service.PublicService { + if input.Catalogue == nil || *input.Catalogue == "" { + return fmt.Errorf("catalogue can not be empty") + } + } + + err = i.serviceService.Save(ctx, id, &service.Edit{ + Name: input.Name, + Description: input.Description, + Logo: input.Logo, + ServiceType: serviceType, + Catalogue: input.Catalogue, + }) + if err != nil { + return err + } + if input.Tags != nil { + tags, err := i.getTagUuids(ctx, *input.Tags) + if err != nil { + return err + } + i.serviceTagService.Delete(ctx, nil, []string{id}) + for _, t := range tags { + err = i.serviceTagService.Create(ctx, &service_tag.CreateTag{ + Tid: t, + Sid: id, + }) + if err != nil { + return err + } + } + } + return nil + }) + + if err != nil { + return nil, err + } + return i.Get(ctx, id) +} + +func (i *imlServiceModule) Delete(ctx context.Context, id string) error { + + err := i.transaction.Transaction(ctx, func(ctx context.Context) error { + count, err := i.apiService.CountByService(ctx, id) + if err != nil { + return err + } + if count > 0 { + return fmt.Errorf("service has apis, can not delete") + } + + return i.serviceService.Delete(ctx, id) + }) + return err +} + +func (i *imlServiceModule) getTagUuids(ctx context.Context, tags []string) ([]string, error) { + list, err := i.tagService.Search(ctx, "", map[string]interface{}{"name": tags}) + if err != nil { + return nil, err + } + tagMap := make(map[string]string) + for _, t := range list { + tagMap[t.Name] = t.Id + } + tagList := make([]string, 0, len(tags)) + repeatTag := make(map[string]struct{}) + for _, t := range tags { + if _, ok := repeatTag[t]; ok { + continue + } + repeatTag[t] = struct{}{} + v := &tag.CreateTag{ + Name: t, + } + id, ok := tagMap[t] + if !ok { + v.Id = uuid.New().String() + err = i.tagService.Create(ctx, v) + if err != nil { + return nil, err + } + tagMap[t] = v.Id + } else { + v.Id = id + } + tagList = append(tagList, v.Id) + } + return tagList, nil +} + +func (i *imlServiceModule) ServiceDoc(ctx context.Context, pid string) (*serviceDto.ServiceDoc, error) { + _, err := i.serviceService.Check(ctx, pid, map[string]bool{"as_server": true}) + + if err != nil { + return nil, err + } + info, err := i.serviceService.Get(ctx, pid) + if err != nil { + return nil, err + } + doc, err := i.serviceDocService.Get(ctx, pid) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + return &serviceDto.ServiceDoc{ + Id: pid, + Name: info.Name, + Doc: "", + }, nil + } + return &serviceDto.ServiceDoc{ + Id: pid, + Name: info.Name, + Doc: doc.Doc, + Creator: auto.UUID(doc.Creator), + CreateTime: auto.TimeLabel(doc.CreateTime), + Updater: auto.UUID(doc.Updater), + UpdateTime: auto.TimeLabel(doc.UpdateTime), + }, nil +} + +func (i *imlServiceModule) SaveServiceDoc(ctx context.Context, pid string, input *serviceDto.SaveServiceDoc) error { + _, err := i.serviceService.Check(ctx, pid, map[string]bool{"as_server": true}) + + if err != nil { + return err + } + return i.serviceDocService.Save(ctx, &service_doc.SaveDoc{ + Sid: pid, + Doc: input.Doc, + }) +} + +var _ IAppModule = &imlAppModule{} + +type imlAppModule struct { + teamService team.ITeamService `autowired:""` + serviceService service.IServiceService `autowired:""` + teamMemberService team_member.ITeamMemberService `autowired:""` + subscribeService subscribe.ISubscribeService `autowired:""` + transaction store.ITransaction `autowired:""` +} + +func (i *imlAppModule) Search(ctx context.Context, teamId string, keyword string) ([]*service_dto.AppItem, error) { + var services []*service.Service + var err error + if teamId != "" { + _, err = i.teamService.Get(ctx, teamId) + if err != nil { + return nil, err + } + services, err = i.serviceService.Search(ctx, keyword, map[string]interface{}{"team": teamId, "as_app": true}, "update_at desc") + } else { + services, err = i.serviceService.Search(ctx, keyword, map[string]interface{}{"as_app": true}, "update_at desc") + } + if err != nil { + return nil, err + } + + serviceIds := utils.SliceToSlice(services, func(p *service.Service) string { + return p.Id + }) + + subscribers, err := i.subscribeService.SubscriptionsByApplication(ctx, serviceIds...) + if err != nil { + return nil, err + } + + subscribeCount := map[string]int64{} + subscribeVerifyCount := map[string]int64{} + verifyTmp := map[string]struct{}{} + subscribeTmp := map[string]struct{}{} + for _, s := range subscribers { + key := fmt.Sprintf("%s-%s", s.Service, s.Application) + switch s.ApplyStatus { + case subscribe.ApplyStatusSubscribe: + if _, ok := subscribeTmp[key]; !ok { + subscribeTmp[key] = struct{}{} + subscribeCount[s.Application]++ + } + case subscribe.ApplyStatusReview: + if _, ok := verifyTmp[key]; !ok { + verifyTmp[key] = struct{}{} + subscribeVerifyCount[s.Application]++ + } + default: + + } + } + items := make([]*service_dto.AppItem, 0, len(services)) + for _, model := range services { + subscribeNum := subscribeCount[model.Id] + verifyNum := subscribeVerifyCount[model.Id] + items = append(items, &service_dto.AppItem{ + Id: model.Id, + Name: model.Name, + Description: model.Description, + CreateTime: auto.TimeLabel(model.CreateTime), + UpdateTime: auto.TimeLabel(model.UpdateTime), + Team: auto.UUID(model.Team), + SubscribeNum: subscribeNum, + SubscribeVerifyNum: verifyNum, + CanDelete: subscribeNum == 0, + }) + } + sort.Slice(items, func(i, j int) bool { + if items[i].SubscribeNum != items[j].SubscribeNum { + return items[i].SubscribeNum > items[j].SubscribeNum + } + if items[i].SubscribeVerifyNum != items[j].SubscribeVerifyNum { + return items[i].SubscribeVerifyNum > items[j].SubscribeVerifyNum + } + return items[i].Name < items[j].Name + }) + return items, nil +} + +func (i *imlAppModule) CreateApp(ctx context.Context, teamID string, input *service_dto.CreateApp) (*service_dto.App, error) { + + if input.Id == "" { + input.Id = uuid.New().String() + } + userId := utils.UserId(ctx) + mo := &service.Create{ + Id: input.Id, + Name: input.Name, + Description: input.Description, + Team: teamID, + AsApp: true, + } + // 判断用户是否在团队内 + members, err := i.teamMemberService.Members(ctx, []string{teamID}, []string{userId}) + if err != nil { + return nil, err + } + if len(members) == 0 { + return nil, fmt.Errorf("master is not in team") + } + + err = i.transaction.Transaction(ctx, func(ctx context.Context) error { + + return i.serviceService.Create(ctx, mo) + + }) + if err != nil { + return nil, err + } + return i.GetApp(ctx, input.Id) +} + +func (i *imlAppModule) UpdateApp(ctx context.Context, appId string, input *service_dto.UpdateApp) (*service_dto.App, error) { + //userId := utils.UserId(ctx) + info, err := i.serviceService.Get(ctx, appId) + if err != nil { + return nil, err + } + if !info.AsApp { + return nil, fmt.Errorf("not app") + } + //if info.Master != userId { + // return nil, fmt.Errorf("user is not app master, can not update") + //} + + err = i.serviceService.Save(ctx, appId, &service.Edit{ + Name: input.Name, + Description: input.Description, + }) + if err != nil { + return nil, err + } + return i.GetApp(ctx, info.Id) +} + +func (i *imlAppModule) searchMyApps(ctx context.Context, teamId string, keyword string) ([]*service.Service, error) { + userID := utils.UserId(ctx) + condition := make(map[string]interface{}) + condition["as_app"] = true + if teamId != "" { + _, err := i.teamService.Get(ctx, teamId) + if err != nil { + return nil, err + } + condition["team"] = teamId + return i.serviceService.Search(ctx, keyword, condition, "update_at desc") + } else { + membersForUser, err := i.teamMemberService.FilterMembersForUser(ctx, userID) + if err != nil { + return nil, err + } + teamIds := membersForUser[userID] + condition["team"] = teamIds + + return i.serviceService.Search(ctx, keyword, condition, "update_at desc") + } +} + +func (i *imlAppModule) SearchMyApps(ctx context.Context, teamId string, keyword string) ([]*service_dto.AppItem, error) { + services, err := i.searchMyApps(ctx, teamId, keyword) + if err != nil { + return nil, err + } + serviceIds := utils.SliceToSlice(services, func(p *service.Service) string { + return p.Id + }) + + subscribers, err := i.subscribeService.SubscriptionsByApplication(ctx, serviceIds...) + if err != nil { + return nil, err + } + + subscribeCount := map[string]int64{} + subscribeVerifyCount := map[string]int64{} + verifyTmp := map[string]struct{}{} + subscribeTmp := map[string]struct{}{} + for _, s := range subscribers { + key := fmt.Sprintf("%s-%s", s.Service, s.Application) + switch s.ApplyStatus { + case subscribe.ApplyStatusSubscribe: + if _, ok := subscribeTmp[key]; !ok { + subscribeTmp[key] = struct{}{} + subscribeCount[s.Application]++ + } + case subscribe.ApplyStatusReview: + if _, ok := verifyTmp[key]; !ok { + verifyTmp[key] = struct{}{} + subscribeVerifyCount[s.Application]++ + } + default: + + } + } + items := make([]*service_dto.AppItem, 0, len(services)) + for _, model := range services { + subscribeNum := subscribeCount[model.Id] + verifyNum := subscribeVerifyCount[model.Id] + items = append(items, &service_dto.AppItem{ + Id: model.Id, + Name: model.Name, + Description: model.Description, + CreateTime: auto.TimeLabel(model.CreateTime), + UpdateTime: auto.TimeLabel(model.UpdateTime), + Team: auto.UUID(model.Team), + SubscribeNum: subscribeNum, + SubscribeVerifyNum: verifyNum, + CanDelete: subscribeNum == 0, + }) + } + sort.Slice(items, func(i, j int) bool { + if items[i].SubscribeNum != items[j].SubscribeNum { + return items[i].SubscribeNum > items[j].SubscribeNum + } + if items[i].SubscribeVerifyNum != items[j].SubscribeVerifyNum { + return items[i].SubscribeVerifyNum > items[j].SubscribeVerifyNum + } + return items[i].Name < items[j].Name + }) + return items, nil +} + +func (i *imlAppModule) SimpleApps(ctx context.Context, keyword string) ([]*service_dto.SimpleAppItem, error) { + w := make(map[string]interface{}) + w["as_app"] = true + services, err := i.serviceService.Search(ctx, keyword, w) + if err != nil { + return nil, err + } + return utils.SliceToSlice(services, func(p *service.Service) *service_dto.SimpleAppItem { + return &service_dto.SimpleAppItem{ + Id: p.Id, + Name: p.Name, + Description: p.Description, + Team: auto.UUID(p.Team), + } + }), nil +} + +func (i *imlAppModule) MySimpleApps(ctx context.Context, keyword string) ([]*service_dto.SimpleAppItem, error) { + services, err := i.searchMyApps(ctx, "", keyword) + if err != nil { + return nil, err + } + items := make([]*service_dto.SimpleAppItem, 0, len(services)) + for _, p := range services { + + items = append(items, &service_dto.SimpleAppItem{ + Id: p.Id, + Name: p.Name, + Description: p.Description, + Team: auto.UUID(p.Team), + }) + } + return items, nil +} + +func (i *imlAppModule) GetApp(ctx context.Context, appId string) (*service_dto.App, error) { + info, err := i.serviceService.Get(ctx, appId) + if err != nil { + return nil, err + } + if !info.AsApp { + return nil, errors.New("not app") + } + return &service_dto.App{ + Id: info.Id, + Name: info.Name, + Description: info.Description, + Team: auto.UUID(info.Team), + CreateTime: auto.TimeLabel(info.CreateTime), + UpdateTime: auto.TimeLabel(info.UpdateTime), + AsApp: info.AsApp, + }, nil +} + +func (i *imlAppModule) DeleteApp(ctx context.Context, appId string) error { + info, err := i.serviceService.Get(ctx, appId) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + return nil + } + if !info.AsApp { + return errors.New("not app, can not delete") + } + + return i.serviceService.Delete(ctx, appId) +} diff --git a/module/service/module.go b/module/service/module.go new file mode 100644 index 00000000..64b7b9c1 --- /dev/null +++ b/module/service/module.go @@ -0,0 +1,57 @@ +package service + +import ( + "context" + "reflect" + + service_dto "github.com/APIParkLab/APIPark/module/service/dto" + + "github.com/eolinker/go-common/autowire" +) + +type IServiceModule interface { + // Get 获取项目信息 + Get(ctx context.Context, id string) (*service_dto.Service, error) + // Search 搜索项目 + Search(ctx context.Context, teamID string, keyword string) ([]*service_dto.ServiceItem, error) + // SearchMyServices 搜索 + SearchMyServices(ctx context.Context, teamId string, keyword string) ([]*service_dto.ServiceItem, error) + // Create 创建 + Create(ctx context.Context, teamID string, input *service_dto.CreateService) (*service_dto.Service, error) + // Edit 编辑 + Edit(ctx context.Context, id string, input *service_dto.EditService) (*service_dto.Service, error) + // Delete 删除项目 + Delete(ctx context.Context, id string) error + // Simple 获取简易项目列表 + Simple(ctx context.Context, keyword string) ([]*service_dto.SimpleServiceItem, error) + + // MySimple 获取我的简易项目列表 + MySimple(ctx context.Context, keyword string) ([]*service_dto.SimpleServiceItem, error) + + ServiceDoc(ctx context.Context, pid string) (*service_dto.ServiceDoc, error) + // SaveServiceDoc 保存服务文档 + SaveServiceDoc(ctx context.Context, pid string, input *service_dto.SaveServiceDoc) error +} + +type IAppModule interface { + CreateApp(ctx context.Context, teamID string, input *service_dto.CreateApp) (*service_dto.App, error) + UpdateApp(ctx context.Context, appId string, input *service_dto.UpdateApp) (*service_dto.App, error) + Search(ctx context.Context, teamId string, keyword string) ([]*service_dto.AppItem, error) + SearchMyApps(ctx context.Context, teamId string, keyword string) ([]*service_dto.AppItem, error) + // SimpleApps 获取简易项目列表 + SimpleApps(ctx context.Context, keyword string) ([]*service_dto.SimpleAppItem, error) + MySimpleApps(ctx context.Context, keyword string) ([]*service_dto.SimpleAppItem, error) + GetApp(ctx context.Context, appId string) (*service_dto.App, error) + DeleteApp(ctx context.Context, appId string) error +} + +func init() { + autowire.Auto[IServiceModule](func() reflect.Value { + m := new(imlServiceModule) + return reflect.ValueOf(m) + }) + autowire.Auto[IAppModule](func() reflect.Value { + return reflect.ValueOf(new(imlAppModule)) + }) + +} diff --git a/module/subscribe/dto/input.go b/module/subscribe/dto/input.go new file mode 100644 index 00000000..56b4d6ad --- /dev/null +++ b/module/subscribe/dto/input.go @@ -0,0 +1,13 @@ +package subscribe_dto + +type AddSubscriber struct { + Application string `json:"application" aocheck:"service"` + Applier string `json:"applier" aocheck:"user"` + //Cluster []string `json:"partition" aocheck:"partition"` +} + +type Approve struct { + //Cluster []string `json:"partition" aocheck:"partition"` + Opinion string `json:"opinion"` + Operate string `json:"operate"` +} diff --git a/module/subscribe/dto/output.go b/module/subscribe/dto/output.go new file mode 100644 index 00000000..d3c8fe07 --- /dev/null +++ b/module/subscribe/dto/output.go @@ -0,0 +1,62 @@ +package subscribe_dto + +import "github.com/eolinker/go-common/auto" + +type Subscriber struct { + Id string `json:"id"` + Service auto.Label `json:"service" aolabel:"service"` + //Cluster []auto.Label `json:"partition" aolabel:"partition"` + + Subscriber auto.Label `json:"subscriber" aolabel:"service"` + Team auto.Label `json:"team" aolabel:"team"` + ApplyTime auto.TimeLabel `json:"apply_time"` + Applier auto.Label `json:"applier" aolabel:"user"` + //Approver auto.Label `json:"approver" aolabel:"user"` + From int `json:"from"` +} + +type SubscriptionItem struct { + Id string `json:"id"` + Service auto.Label `json:"service" aolabel:"service"` + //Cluster auto.Label `json:"partition" aolabel:"partition"` + ApplyStatus int `json:"apply_status"` + Team auto.Label `json:"team" aolabel:"team"` + //Applier auto.Label `json:"applier" aolabel:"user"` + From int `json:"from"` + CreateTime auto.TimeLabel `json:"create_time"` +} + +type Approval struct { + Id string `json:"id,omitempty"` + Service auto.Label `json:"service" aolabel:"service"` + Team auto.Label `json:"team" aolabel:"team"` + Application auto.Label `json:"application" aolabel:"service"` + ApplyTeam auto.Label `json:"apply_team" aolabel:"team"` + ApplyTime auto.TimeLabel `json:"apply_time"` + Applier auto.Label `json:"applier" aolabel:"user"` + Approver auto.Label `json:"approver" aolabel:"user"` + ApprovalTime auto.TimeLabel `json:"approval_time"` + Reason string `json:"reason"` + Opinion string `json:"opinion"` + Status int `json:"status"` +} + +type ApprovalItem struct { + Id string `json:"id"` + Service auto.Label `json:"service" aolabel:"service"` + Team auto.Label `json:"team" aolabel:"team"` + Application auto.Label `json:"application" aolabel:"service"` + ApplyTeam auto.Label `json:"apply_team" aolabel:"team"` + ApplyTime auto.TimeLabel `json:"apply_time"` + Applier auto.Label `json:"applier" aolabel:"user"` + Approver auto.Label `json:"approver" aolabel:"user"` + ApprovalTime auto.TimeLabel `json:"approval_time"` + Status int `json:"status"` +} + +// +//type PartitionServiceItem struct { +// Id string `json:"id"` +// Name string `json:"name"` +// ServiceNum int64 `json:"service_num"` +//} diff --git a/module/subscribe/iml.go b/module/subscribe/iml.go new file mode 100644 index 00000000..afa6f1d4 --- /dev/null +++ b/module/subscribe/iml.go @@ -0,0 +1,478 @@ +package subscribe + +import ( + "context" + "fmt" + + "github.com/google/uuid" + + "github.com/eolinker/eosc/log" + + "github.com/APIParkLab/APIPark/gateway" + + "github.com/APIParkLab/APIPark/service/cluster" + + "github.com/eolinker/go-common/utils" + + "github.com/APIParkLab/APIPark/service/service" + + "github.com/eolinker/go-common/store" + + "github.com/eolinker/go-common/auto" + + "github.com/APIParkLab/APIPark/service/subscribe" + + subscribe_dto "github.com/APIParkLab/APIPark/module/subscribe/dto" +) + +var ( + _ ISubscribeModule = (*imlSubscribeModule)(nil) +) + +type imlSubscribeModule struct { + serviceService service.IServiceService `autowired:""` + subscribeService subscribe.ISubscribeService `autowired:""` + subscribeApplyService subscribe.ISubscribeApplyService `autowired:""` + clusterService cluster.IClusterService `autowired:""` + transaction store.ITransaction `autowired:""` +} + +func (i *imlSubscribeModule) getSubscribers(ctx context.Context, serviceIds []string) ([]*gateway.SubscribeRelease, error) { + subscribers, err := i.subscribeService.SubscribersByProject(ctx, serviceIds...) + if err != nil { + return nil, err + } + return utils.SliceToSlice(subscribers, func(s *subscribe.Subscribe) *gateway.SubscribeRelease { + return &gateway.SubscribeRelease{ + Service: s.Service, + Application: s.Application, + Expired: "0", + } + }), nil +} + +func (i *imlSubscribeModule) initGateway(ctx context.Context, clientDriver gateway.IClientDriver) error { + + projects, err := i.serviceService.List(ctx) + if err != nil { + return err + } + serviceIds := utils.SliceToSlice(projects, func(p *service.Service) string { + return p.Id + }) + releases, err := i.getSubscribers(ctx, serviceIds) + if err != nil { + return err + } + + return clientDriver.Subscribe().Online(ctx, releases...) +} + +func (i *imlSubscribeModule) SearchSubscriptions(ctx context.Context, appId string, keyword string) ([]*subscribe_dto.SubscriptionItem, error) { + info, err := i.serviceService.Get(ctx, appId) + if err != nil { + return nil, fmt.Errorf("get application error: %w", err) + } + if !info.AsApp { + return nil, fmt.Errorf("service %s is not an application", appId) + } + + // 获取当前订阅服务列表 + subscriptions, err := i.subscribeService.MySubscribeServices(ctx, appId, nil) + if err != nil { + return nil, err + } + serviceIds := utils.SliceToSlice(subscriptions, func(s *subscribe.Subscribe) string { + return s.Service + }) + services, err := i.serviceService.List(ctx, serviceIds...) + if err != nil { + return nil, fmt.Errorf("search service error: %w", err) + } + serviceMap := utils.SliceToMapArray(services, func(s *service.Service) string { + return s.Id + }) + + return utils.SliceToSlice(subscriptions, func(s *subscribe.Subscribe) *subscribe_dto.SubscriptionItem { + return &subscribe_dto.SubscriptionItem{ + Id: s.Id, + ApplyStatus: s.ApplyStatus, + Service: auto.UUID(s.Service), + Team: auto.UUID(info.Team), + From: s.From, + CreateTime: auto.TimeLabel(s.CreateAt), + } + }, func(s *subscribe.Subscribe) bool { + _, ok := serviceMap[s.Service] + if !ok { + return false + } + if s.ApplyStatus != subscribe.ApplyStatusSubscribe && s.ApplyStatus != subscribe.ApplyStatusReview { + return false + } + return true + }), nil +} + +func (i *imlSubscribeModule) RevokeSubscription(ctx context.Context, pid string, uuid string) error { + _, err := i.serviceService.Get(ctx, pid) + if err != nil { + return fmt.Errorf("get service error: %w", err) + } + subscription, err := i.subscribeService.Get(ctx, uuid) + if err != nil { + return err + } + if subscription.ApplyStatus != subscribe.ApplyStatusSubscribe { + return fmt.Errorf("subscription can not be revoked") + } + + clusters, err := i.clusterService.List(ctx) + if err != nil { + return err + } + applyStatus := subscribe.ApplyStatusUnsubscribe + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + err = i.subscribeService.Save(ctx, uuid, &subscribe.UpdateSubscribe{ + ApplyStatus: &applyStatus, + }) + if err != nil { + return err + } + for _, c := range clusters { + err = i.offlineForCluster(ctx, c.Uuid, &gateway.SubscribeRelease{ + Service: subscription.Service, + Application: subscription.Application, + }) + if err != nil { + return err + } + } + + return nil + }) + +} + +func (i *imlSubscribeModule) DeleteSubscription(ctx context.Context, pid string, uuid string) error { + _, err := i.serviceService.Get(ctx, pid) + if err != nil { + return fmt.Errorf("get service error: %w", err) + } + subscription, err := i.subscribeService.Get(ctx, uuid) + if err != nil { + return err + } + if subscription.ApplyStatus == subscribe.ApplyStatusSubscribe || subscription.ApplyStatus == subscribe.ApplyStatusReview { + return fmt.Errorf("subscription can not be deleted") + } + return i.subscribeService.Delete(ctx, uuid) +} + +func (i *imlSubscribeModule) RevokeApply(ctx context.Context, app string, uuid string) error { + _, err := i.serviceService.Get(ctx, app) + if err != nil { + return fmt.Errorf("get app error: %w", err) + } + subscription, err := i.subscribeService.Get(ctx, uuid) + if err != nil { + return err + } + if subscription.ApplyStatus != subscribe.ApplyStatusReview { + return fmt.Errorf("apply can not be revoked") + } + applyStatus := subscribe.ApplyStatusCancel + return i.subscribeService.Save(ctx, uuid, &subscribe.UpdateSubscribe{ + ApplyStatus: &applyStatus, + }) +} + +func (i *imlSubscribeModule) AddSubscriber(ctx context.Context, serviceId string, input *subscribe_dto.AddSubscriber) error { + _, err := i.serviceService.Get(ctx, serviceId) + if err != nil { + return err + } + + sub := &gateway.SubscribeRelease{ + Service: serviceId, + Application: input.Application, + Expired: "0", + } + clusters, err := i.clusterService.List(ctx) + if err != nil { + return err + } + + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + err = i.subscribeService.Create(ctx, &subscribe.CreateSubscribe{ + Uuid: uuid.New().String(), + Service: serviceId, + Application: input.Application, + ApplyStatus: subscribe.ApplyStatusSubscribe, + From: subscribe.FromUser, + Applier: input.Applier, + }) + if err != nil { + return err + } + for _, c := range clusters { + err = i.onlineSubscriber(ctx, c.Uuid, sub) + if err != nil { + return fmt.Errorf("add subscriber for cluster[%s] %v", c.Uuid, err) + } + } + + return nil + }) + +} + +func (i *imlSubscribeModule) onlineSubscriber(ctx context.Context, clusterId string, subscriber *gateway.SubscribeRelease) error { + + client, err := i.clusterService.GatewayClient(ctx, clusterId) + if err != nil { + return err + } + defer func() { + _ = client.Close(ctx) + }() + return client.Subscribe().Online(ctx, subscriber) + +} + +func (i *imlSubscribeModule) DeleteSubscriber(ctx context.Context, service string, serviceId string, applicationId string) error { + _, err := i.serviceService.Get(ctx, service) + if err != nil { + return err + } + clusters, err := i.clusterService.List(ctx) + if err != nil { + return err + } + + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + list, err := i.subscribeService.ListByApplication(ctx, serviceId, applicationId) + if err != nil { + return err + } + releaseInfo := &gateway.SubscribeRelease{ + Service: serviceId, + Application: applicationId, + } + for _, s := range list { + err = i.subscribeService.Delete(ctx, s.Id) + if err != nil { + return err + } + } + for _, c := range clusters { + err = i.offlineForCluster(ctx, c.Uuid, releaseInfo) + if err != nil { + return fmt.Errorf("offline subscribe for cluster[%s] %s", c.Uuid, err) + } + } + return nil + }) +} +func (i *imlSubscribeModule) offlineForCluster(ctx context.Context, clusterId string, config *gateway.SubscribeRelease) error { + + client, err := i.clusterService.GatewayClient(ctx, clusterId) + if err != nil { + return err + } + defer func() { + _ = client.Close(ctx) + }() + return client.Subscribe().Offline(ctx, config) +} + +func (i *imlSubscribeModule) SearchSubscribers(ctx context.Context, serviceId string, keyword string) ([]*subscribe_dto.Subscriber, error) { + pInfo, err := i.serviceService.Get(ctx, serviceId) + if err != nil { + return nil, err + } + + // 获取当前项目所有订阅方 + list, err := i.subscribeService.ListBySubscribeStatus(ctx, serviceId, subscribe.ApplyStatusSubscribe) + if err != nil { + return nil, err + } + + if keyword == "" { + items := make([]*subscribe_dto.Subscriber, 0, len(list)) + for _, subscriber := range list { + items = append(items, &subscribe_dto.Subscriber{ + Id: subscriber.Application, + Service: auto.UUID(subscriber.Service), + Subscriber: auto.UUID(subscriber.Application), + Team: auto.UUID(pInfo.Team), + Applier: auto.UUID(subscriber.Applier), + ApplyTime: auto.TimeLabel(subscriber.CreateAt), + From: subscriber.From, + }) + } + return items, nil + } + serviceList, err := i.serviceService.Search(ctx, keyword, map[string]interface{}{ + "service": serviceId, + }) + if err != nil { + return nil, err + } + serviceMap := utils.SliceToMap(serviceList, func(s *service.Service) string { + return s.Id + }) + items := make([]*subscribe_dto.Subscriber, 0, len(list)) + for _, subscriber := range list { + + if _, ok := serviceMap[subscriber.Service]; ok { + items = append(items, &subscribe_dto.Subscriber{ + Id: subscriber.Id, + Service: auto.UUID(subscriber.Service), + Subscriber: auto.UUID(subscriber.Application), + Team: auto.UUID(pInfo.Team), + ApplyTime: auto.TimeLabel(subscriber.CreateAt), + From: subscriber.From, + }) + } + } + return items, nil +} + +var _ ISubscribeApprovalModule = (*imlSubscribeApprovalModule)(nil) + +type imlSubscribeApprovalModule struct { + subscribeService subscribe.ISubscribeService `autowired:""` + subscribeApplyService subscribe.ISubscribeApplyService `autowired:""` + projectService service.IServiceService `autowired:""` + clusterService cluster.IClusterService `autowired:""` + transaction store.ITransaction `autowired:""` +} + +func (i *imlSubscribeApprovalModule) Pass(ctx context.Context, pid string, id string, approveInfo *subscribe_dto.Approve) error { + applyInfo, err := i.subscribeApplyService.Get(ctx, id) + if err != nil { + return err + } + + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + userID := utils.UserId(ctx) + status := subscribe.ApplyStatusSubscribe + err = i.subscribeApplyService.Save(ctx, id, &subscribe.EditApply{ + Opinion: &approveInfo.Opinion, + Status: &status, + Approver: &userID, + }) + if err != nil { + return err + } + err = i.subscribeService.UpdateSubscribeStatus(ctx, applyInfo.Application, applyInfo.Service, status) + if err != nil { + return err + } + cs, err := i.clusterService.List(ctx) + if err != nil { + return err + } + for _, c := range cs { + + err := i.onlineSubscriber(ctx, c.Uuid, &gateway.SubscribeRelease{ + Service: applyInfo.Service, + Application: applyInfo.Application, + Expired: "0", + }) + + if err != nil { + log.Warnf("online subscriber for cluster[%s] %v", c.Uuid, err) + + } + } + return nil + }) +} +func (i *imlSubscribeApprovalModule) onlineSubscriber(ctx context.Context, clusterId string, sub *gateway.SubscribeRelease) error { + client, err := i.clusterService.GatewayClient(ctx, clusterId) + if err != nil { + return err + } + defer func() { + _ = client.Close(ctx) + }() + return client.Subscribe().Online(ctx, sub) +} +func (i *imlSubscribeApprovalModule) Reject(ctx context.Context, pid string, id string, approveInfo *subscribe_dto.Approve) error { + _, err := i.subscribeApplyService.Get(ctx, id) + if err != nil { + return err + } + + return i.transaction.Transaction(ctx, func(ctx context.Context) error { + userID := utils.UserId(ctx) + status := subscribe.ApplyStatusRefuse + err = i.subscribeApplyService.Save(ctx, id, &subscribe.EditApply{ + Opinion: &approveInfo.Opinion, + Status: &status, + Approver: &userID, + }) + if err != nil { + return err + } + return nil + //return i.subscribeService.UpdateSubscribeStatus(ctx, applyInfo.Application, applyInfo.Service, status) + }) +} + +func (i *imlSubscribeApprovalModule) GetApprovalList(ctx context.Context, pid string, status int) ([]*subscribe_dto.ApprovalItem, error) { + applyStatus := make([]int, 0, 2) + if status == 0 { + // 获取待审批列表 + applyStatus = append(applyStatus, subscribe.ApplyStatusReview) + } else { + // 获取已审批列表 + applyStatus = append(applyStatus, subscribe.ApplyStatusRefuse, subscribe.ApplyStatusSubscribe) + } + items, err := i.subscribeApplyService.ListByStatus(ctx, pid, applyStatus...) + if err != nil { + return nil, err + } + return utils.SliceToSlice(items, func(s *subscribe.Apply) *subscribe_dto.ApprovalItem { + return &subscribe_dto.ApprovalItem{ + Id: s.Id, + Service: auto.UUID(s.Service), + Team: auto.UUID(s.Team), + Application: auto.UUID(s.Application), + ApplyTeam: auto.UUID(s.ApplyTeam), + ApplyTime: auto.TimeLabel(s.ApplyAt), + Applier: auto.UUID(s.Applier), + Approver: auto.UUID(s.Approver), + ApprovalTime: auto.TimeLabel(s.ApproveAt), + Status: s.Status, + } + }), nil +} + +func (i *imlSubscribeApprovalModule) GetApprovalDetail(ctx context.Context, pid string, id string) (*subscribe_dto.Approval, error) { + _, err := i.projectService.Get(ctx, pid) + if err != nil { + return nil, err + } + item, err := i.subscribeApplyService.Get(ctx, id) + if err != nil { + return nil, err + } + + return &subscribe_dto.Approval{ + Id: item.Id, + Service: auto.UUID(item.Service), + Team: auto.UUID(item.Team), + Application: auto.UUID(item.Application), + ApplyTeam: auto.UUID(item.ApplyTeam), + ApplyTime: auto.TimeLabel(item.ApplyAt), + Applier: auto.UUID(item.Applier), + Approver: auto.UUID(item.Approver), + ApprovalTime: auto.TimeLabel(item.ApproveAt), + Reason: item.Reason, + Opinion: item.Opinion, + Status: item.Status, + }, nil +} diff --git a/module/subscribe/subscribe.go b/module/subscribe/subscribe.go new file mode 100644 index 00000000..5346295f --- /dev/null +++ b/module/subscribe/subscribe.go @@ -0,0 +1,49 @@ +package subscribe + +import ( + "context" + "reflect" + + "github.com/eolinker/go-common/autowire" + + subscribe_dto "github.com/APIParkLab/APIPark/module/subscribe/dto" +) + +type ISubscribeModule interface { + // AddSubscriber 新增订阅方 + AddSubscriber(ctx context.Context, pid string, input *subscribe_dto.AddSubscriber) error + // DeleteSubscriber 删除订阅方 + DeleteSubscriber(ctx context.Context, project string, serviceId string, applicationId string) error + // SearchSubscribers 关键字获取订阅方列表 + SearchSubscribers(ctx context.Context, pid string, keyword string) ([]*subscribe_dto.Subscriber, error) + // SearchSubscriptions 关键字获取订阅服务列表 + SearchSubscriptions(ctx context.Context, appId string, keyword string) ([]*subscribe_dto.SubscriptionItem, error) + // RevokeSubscription 取消订阅 + RevokeSubscription(ctx context.Context, pid string, uuid string) error + // DeleteSubscription 删除订阅 + DeleteSubscription(ctx context.Context, pid string, uuid string) error + // RevokeApply 取消申请 + RevokeApply(ctx context.Context, app string, uuid string) error + //PartitionServices(ctx context.Context, app string) ([]*subscribe_dto.PartitionServiceItem, error) +} + +type ISubscribeApprovalModule interface { + // GetApprovalList 获取审批列表 + GetApprovalList(ctx context.Context, pid string, status int) ([]*subscribe_dto.ApprovalItem, error) + // GetApprovalDetail 获取审批详情 + GetApprovalDetail(ctx context.Context, pid string, id string) (*subscribe_dto.Approval, error) + // Pass 通过审批 + Pass(ctx context.Context, pid string, id string, approveInfo *subscribe_dto.Approve) error + // Reject 驳回审批 + Reject(ctx context.Context, pid string, id string, approveInfo *subscribe_dto.Approve) error +} + +func init() { + autowire.Auto[ISubscribeModule](func() reflect.Value { + return reflect.ValueOf(new(imlSubscribeModule)) + }) + + autowire.Auto[ISubscribeApprovalModule](func() reflect.Value { + return reflect.ValueOf(new(imlSubscribeApprovalModule)) + }) +} diff --git a/module/tag/dto/input.go b/module/tag/dto/input.go new file mode 100644 index 00000000..4ec315f2 --- /dev/null +++ b/module/tag/dto/input.go @@ -0,0 +1,6 @@ +package tag_dto + +type CreateTag struct { + Id string `json:"id"` + Name string `json:"name"` +} diff --git a/module/tag/dto/output.go b/module/tag/dto/output.go new file mode 100644 index 00000000..ddec0ea4 --- /dev/null +++ b/module/tag/dto/output.go @@ -0,0 +1,6 @@ +package tag_dto + +type Item struct { + Id string `json:"id"` + Name string `json:"name"` +} diff --git a/module/tag/iml.go b/module/tag/iml.go new file mode 100644 index 00000000..0f4277da --- /dev/null +++ b/module/tag/iml.go @@ -0,0 +1,48 @@ +package tag + +import ( + "context" + + "github.com/google/uuid" + + "github.com/eolinker/go-common/utils" + + tag_dto "github.com/APIParkLab/APIPark/module/tag/dto" + "github.com/APIParkLab/APIPark/service/tag" +) + +var ( + _ ITagModule = (*imlTagModule)(nil) +) + +type imlTagModule struct { + tagService tag.ITagService `autowired:""` +} + +func (i *imlTagModule) Search(ctx context.Context, keyword string) ([]*tag_dto.Item, error) { + items, err := i.tagService.Search(ctx, keyword, nil) + if err != nil { + return nil, err + } + out := utils.SliceToSlice(items, func(item *tag.Tag) *tag_dto.Item { + return &tag_dto.Item{ + Id: item.Id, + Name: item.Name, + } + }) + return out, nil +} + +func (i *imlTagModule) Create(ctx context.Context, input *tag_dto.CreateTag) error { + if input.Id == "" { + input.Id = uuid.New().String() + } + return i.tagService.Create(ctx, &tag.CreateTag{ + Id: input.Id, + Name: input.Name, + }) +} + +func (i *imlTagModule) Delete(ctx context.Context, id string) error { + return i.tagService.Delete(ctx, id) +} diff --git a/module/tag/tag.go b/module/tag/tag.go new file mode 100644 index 00000000..eca7248e --- /dev/null +++ b/module/tag/tag.go @@ -0,0 +1,25 @@ +package tag + +import ( + "context" + "reflect" + + tag_dto "github.com/APIParkLab/APIPark/module/tag/dto" + + "github.com/eolinker/go-common/autowire" +) + +type ITagModule interface { + // Search 搜索标签 + Search(ctx context.Context, keyword string) ([]*tag_dto.Item, error) + // Create 创建标签 + Create(ctx context.Context, input *tag_dto.CreateTag) error + // Delete 删除标签 + Delete(ctx context.Context, id string) error +} + +func init() { + autowire.Auto[ITagModule](func() reflect.Value { + return reflect.ValueOf(new(imlTagModule)) + }) +} diff --git a/module/team/dto/input.go b/module/team/dto/input.go new file mode 100644 index 00000000..040d202f --- /dev/null +++ b/module/team/dto/input.go @@ -0,0 +1,12 @@ +package team_dto + +type CreateTeam struct { + Id string `json:"id"` + Name string `json:"name" binding:"required"` + Description string `json:"description"` + Master string `json:"master"` +} +type EditTeam struct { + Name *string `json:"name"` + Description *string `json:"description"` +} diff --git a/module/team/dto/output.go b/module/team/dto/output.go new file mode 100644 index 00000000..0b20ffe5 --- /dev/null +++ b/module/team/dto/output.go @@ -0,0 +1,54 @@ +package team_dto + +import ( + "github.com/APIParkLab/APIPark/service/team" + "github.com/eolinker/go-common/auto" +) + +type Item struct { + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + CreateTime auto.TimeLabel `json:"create_time"` + UpdateTime auto.TimeLabel `json:"update_time"` + CanDelete bool `json:"can_delete"` + ServiceNum int64 `json:"service_num"` + AppNum int64 `json:"app_num"` +} + +func ToItem(model *team.Team, serviceNum int64, appNum int64) *Item { + return &Item{ + Id: model.Id, + Name: model.Name, + Description: model.Description, + CreateTime: auto.TimeLabel(model.CreateTime), + UpdateTime: auto.TimeLabel(model.UpdateTime), + ServiceNum: serviceNum, + AppNum: appNum, + CanDelete: serviceNum == 0 && appNum == 0, + } +} + +type Team struct { + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + CreateTime auto.TimeLabel `json:"create_time"` + UpdateTime auto.TimeLabel `json:"update_time"` + Creator auto.Label `json:"creator" aolabel:"user"` + Updater auto.Label `json:"updater" aolabel:"user"` + CanDelete bool `json:"can_delete"` +} + +func ToTeam(model *team.Team, serviceNum int64, appNum int64) *Team { + return &Team{ + Id: model.Id, + Name: model.Name, + Description: model.Description, + CreateTime: auto.TimeLabel(model.CreateTime), + UpdateTime: auto.TimeLabel(model.UpdateTime), + Creator: auto.UUID(model.Creator), + Updater: auto.UUID(model.Updater), + CanDelete: serviceNum == 0 && appNum == 0, + } +} diff --git a/module/team/iml.go b/module/team/iml.go new file mode 100644 index 00000000..2310c5f8 --- /dev/null +++ b/module/team/iml.go @@ -0,0 +1,148 @@ +package team + +import ( + "context" + "fmt" + + "github.com/eolinker/go-common/utils" + + "github.com/eolinker/ap-account/service/role" + + "github.com/eolinker/go-common/store" + + "github.com/eolinker/ap-account/service/user" + + "github.com/APIParkLab/APIPark/service/service" + team_member "github.com/APIParkLab/APIPark/service/team-member" + + "github.com/google/uuid" + + team_dto "github.com/APIParkLab/APIPark/module/team/dto" + "github.com/APIParkLab/APIPark/service/team" +) + +var ( + _ ITeamModule = (*imlTeamModule)(nil) +) + +type imlTeamModule struct { + service team.ITeamService `autowired:""` + memberService team_member.ITeamMemberService `autowired:""` + userService user.IUserService `autowired:""` + serviceService service.IServiceService `autowired:""` + roleService role.IRoleService `autowired:""` + roleMemberService role.IRoleMemberService `autowired:""` + transaction store.ITransaction `autowired:""` +} + +func (m *imlTeamModule) GetTeam(ctx context.Context, id string) (*team_dto.Team, error) { + tv, err := m.service.Get(ctx, id) + if err != nil { + return nil, err + } + serviceCountMap, err := m.serviceService.ServiceCountByTeam(ctx, id) + if err != nil { + return nil, err + } + appCountMap, err := m.serviceService.ServiceCountByTeam(ctx, id) + if err != nil { + return nil, err + } + + return team_dto.ToTeam(tv, serviceCountMap[id], appCountMap[id]), nil + +} + +func (m *imlTeamModule) Search(ctx context.Context, keyword string) ([]*team_dto.Item, error) { + list, err := m.service.Search(ctx, keyword, nil) + if err != nil { + return nil, err + } + + serviceCountMap, err := m.serviceService.ServiceCountByTeam(ctx) + if err != nil { + return nil, err + } + appCountMap, err := m.serviceService.AppCountByTeam(ctx) + if err != nil { + return nil, err + } + outList := make([]*team_dto.Item, 0, len(list)) + for _, v := range list { + outList = append(outList, team_dto.ToItem(v, serviceCountMap[v.Id], appCountMap[v.Id])) + } + return outList, nil +} + +func (m *imlTeamModule) Create(ctx context.Context, input *team_dto.CreateTeam) (*team_dto.Team, error) { + if input.Id == "" { + input.Id = uuid.New().String() + } + + err := m.transaction.Transaction(ctx, func(ctx context.Context) error { + if input.Master == "" { + input.Master = utils.UserId(ctx) + } + err := m.service.Create(ctx, &team.CreateTeam{ + Id: input.Id, + Name: input.Name, + Description: input.Description, + }) + if err != nil { + return err + } + + err = m.memberService.AddMemberTo(ctx, input.Id, input.Master) + if err != nil { + return err + } + supperRole, err := m.roleService.GetSupperRole(ctx, role.GroupTeam) + if err != nil { + return err + } + + return m.roleMemberService.Add(ctx, &role.AddMember{ + Role: supperRole.Id, + User: input.Master, + Target: role.TeamTarget(input.Id), + }) + }) + if err != nil { + return nil, err + } + return m.GetTeam(ctx, input.Id) +} + +func (m *imlTeamModule) Edit(ctx context.Context, id string, input *team_dto.EditTeam) (*team_dto.Team, error) { + err := m.transaction.Transaction(ctx, func(ctx context.Context) error { + return m.service.Save(ctx, id, &team.EditTeam{ + Name: input.Name, + Description: input.Description, + }) + }) + + if err != nil { + return nil, err + } + return m.GetTeam(ctx, id) +} + +func (m *imlTeamModule) Delete(ctx context.Context, id string) error { + err := m.transaction.Transaction(ctx, func(ctx context.Context) error { + count, err := m.serviceService.Count(ctx, "", map[string]interface{}{ + "team": id, + }) + if err != nil { + return err + } + if count != 0 { + return fmt.Errorf("team has projects,cannot delete") + } + err = m.service.Delete(ctx, id) + if err != nil { + return err + } + return nil + }) + return err +} diff --git a/module/team/team.go b/module/team/team.go new file mode 100644 index 00000000..584e5059 --- /dev/null +++ b/module/team/team.go @@ -0,0 +1,29 @@ +package team + +import ( + "context" + "reflect" + + team_dto "github.com/APIParkLab/APIPark/module/team/dto" + "github.com/eolinker/go-common/autowire" +) + +type ITeamModule interface { + // GetTeam 获取团队信息 + GetTeam(ctx context.Context, id string) (*team_dto.Team, error) + // Search 搜索团队 + Search(ctx context.Context, keyword string) ([]*team_dto.Item, error) + + // Create 创建团队 + Create(ctx context.Context, input *team_dto.CreateTeam) (*team_dto.Team, error) + // Edit 编辑团队 + Edit(ctx context.Context, id string, input *team_dto.EditTeam) (*team_dto.Team, error) + // Delete 删除团队 + Delete(ctx context.Context, id string) error +} + +func init() { + autowire.Auto[ITeamModule](func() reflect.Value { + return reflect.ValueOf(new(imlTeamModule)) + }) +} diff --git a/module/upstream/dto/input.go b/module/upstream/dto/input.go new file mode 100644 index 00000000..44f1fa46 --- /dev/null +++ b/module/upstream/dto/input.go @@ -0,0 +1,85 @@ +package upstream_dto + +import ( + "github.com/APIParkLab/APIPark/service/upstream" +) + +func FromProxyHeaders(p []*upstream.ProxyHeader) []*ProxyHeader { + proxyHeaders := make([]*ProxyHeader, 0, len(p)) + for _, header := range p { + proxyHeaders = append(proxyHeaders, &ProxyHeader{ + Key: header.Key, + Value: header.Value, + OptType: header.OptType, + }) + } + return proxyHeaders +} + +func ToProxyHeaders(p []*ProxyHeader) []*upstream.ProxyHeader { + proxyHeaders := make([]*upstream.ProxyHeader, 0, len(p)) + for _, header := range p { + proxyHeaders = append(proxyHeaders, &upstream.ProxyHeader{ + Key: header.Key, + Value: header.Value, + OptType: header.OptType, + }) + } + return proxyHeaders +} + +func ConvertUpstream(u *Upstream) *upstream.Config { + nodes := make([]*upstream.NodeConfig, 0, len(u.Nodes)) + for _, node := range u.Nodes { + nodes = append(nodes, &upstream.NodeConfig{ + Address: node.Address, + Weight: node.Weight, + }) + } + discover := &upstream.DiscoverConfig{} + if u.Discover != nil { + discover.Discover = u.Discover.Discover + discover.Service = u.Discover.Service + } + return &upstream.Config{ + Balance: u.Balance, + Timeout: u.Timeout, + Retry: u.Retry, + Type: u.Type, + LimitPeerSecond: u.LimitPeerSecond, + ProxyHeaders: ToProxyHeaders(u.ProxyHeaders), + Scheme: u.Scheme, + PassHost: u.PassHost, + UpstreamHost: u.UpstreamHost, + Nodes: nodes, + Discover: discover, + } +} + +func FromClusterConfig(c *upstream.Config) *Upstream { + nodes := make([]*NodeConfig, 0, len(c.Nodes)) + for _, node := range c.Nodes { + nodes = append(nodes, &NodeConfig{ + Address: node.Address, + Weight: node.Weight, + }) + } + discover := &DiscoverConfig{} + if c.Discover != nil { + discover.Discover = c.Discover.Discover + discover.Service = c.Discover.Service + } + return &Upstream{ + Type: c.Type, + Balance: c.Balance, + Timeout: c.Timeout, + Retry: c.Retry, + LimitPeerSecond: c.LimitPeerSecond, + ProxyHeaders: FromProxyHeaders(c.ProxyHeaders), + Scheme: c.Scheme, + PassHost: c.PassHost, + UpstreamHost: c.UpstreamHost, + Nodes: nodes, + Discover: discover, + } +} diff --git a/module/upstream/dto/output.go b/module/upstream/dto/output.go new file mode 100644 index 00000000..ade127c4 --- /dev/null +++ b/module/upstream/dto/output.go @@ -0,0 +1,34 @@ +package upstream_dto + +type UpstreamConfig *Upstream + +type Upstream struct { + Type string `json:"driver"` + Balance string `json:"balance"` + Timeout int `json:"timeout"` + Retry int `json:"retry"` + Remark string `json:"remark"` + LimitPeerSecond int `json:"limit_peer_second"` + ProxyHeaders []*ProxyHeader `json:"proxy_headers"` + Scheme string `json:"scheme"` + PassHost string `json:"pass_host"` + UpstreamHost string `json:"upstream_host"` + Nodes []*NodeConfig `json:"nodes"` + Discover *DiscoverConfig `json:"discover"` +} + +type NodeConfig struct { + Address string `json:"address"` + Weight int `json:"weight"` +} + +type DiscoverConfig struct { + Discover string `json:"discover"` + Service string `json:"service"` +} + +type ProxyHeader struct { + Key string `json:"key"` + Value string `json:"value"` + OptType string `json:"opt_type"` +} diff --git a/module/upstream/iml.go b/module/upstream/iml.go new file mode 100644 index 00000000..516a6588 --- /dev/null +++ b/module/upstream/iml.go @@ -0,0 +1,80 @@ +package upstream + +import ( + "context" + "errors" + "fmt" + + "github.com/APIParkLab/APIPark/service/cluster" + "github.com/APIParkLab/APIPark/service/service" + + "gorm.io/gorm" + + "github.com/APIParkLab/APIPark/service/upstream" + + "github.com/eolinker/go-common/store" + + upstream_dto "github.com/APIParkLab/APIPark/module/upstream/dto" +) + +var ( + _ IUpstreamModule = (*imlUpstreamModule)(nil) + asServer = map[string]bool{ + "as_server": true, + } +) + +type imlUpstreamModule struct { + projectService service.IServiceService `autowired:""` + upstreamService upstream.IUpstreamService `autowired:""` + transaction store.ITransaction `autowired:""` +} + +func (i *imlUpstreamModule) Get(ctx context.Context, pid string) (upstream_dto.UpstreamConfig, error) { + _, err := i.projectService.Check(ctx, pid, asServer) + if err != nil { + return nil, err + } + _, err = i.upstreamService.Get(ctx, pid) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + return nil, nil + } + commit, err := i.upstreamService.LatestCommit(ctx, pid, cluster.DefaultClusterID) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + return nil, nil + } + + return upstream_dto.FromClusterConfig(commit.Data), nil +} + +func (i *imlUpstreamModule) Save(ctx context.Context, pid string, upstreamConfig upstream_dto.UpstreamConfig) (upstream_dto.UpstreamConfig, error) { + pInfo, err := i.projectService.Check(ctx, pid, asServer) + if err != nil { + return nil, err + } + + err = i.transaction.Transaction(ctx, func(ctx context.Context) error { + err = i.upstreamService.SaveCommit(ctx, pid, cluster.DefaultClusterID, upstream_dto.ConvertUpstream(upstreamConfig)) + if err != nil { + return err + } + + return i.upstreamService.Save(ctx, &upstream.SaveUpstream{ + UUID: pid, + Name: fmt.Sprintf("upstream-%s", pid), + Project: pid, + Team: pInfo.Team, + }) + + }) + if err != nil { + return nil, err + } + return i.Get(ctx, pid) +} diff --git a/module/upstream/upstream.go b/module/upstream/upstream.go new file mode 100644 index 00000000..c5c93c21 --- /dev/null +++ b/module/upstream/upstream.go @@ -0,0 +1,21 @@ +package upstream + +import ( + "context" + "reflect" + + "github.com/eolinker/go-common/autowire" + + upstream_dto "github.com/APIParkLab/APIPark/module/upstream/dto" +) + +type IUpstreamModule interface { + Get(ctx context.Context, pid string) (upstream_dto.UpstreamConfig, error) + Save(ctx context.Context, pid string, upstream upstream_dto.UpstreamConfig) (upstream_dto.UpstreamConfig, error) +} + +func init() { + autowire.Auto[IUpstreamModule](func() reflect.Value { + return reflect.ValueOf(new(imlUpstreamModule)) + }) +} diff --git a/plugins/core/access.go b/plugins/core/access.go new file mode 100644 index 00000000..9a8bc959 --- /dev/null +++ b/plugins/core/access.go @@ -0,0 +1 @@ +package core diff --git a/plugins/core/api.go b/plugins/core/api.go new file mode 100644 index 00000000..7bd016ec --- /dev/null +++ b/plugins/core/api.go @@ -0,0 +1,24 @@ +package core + +import ( + "net/http" + + "github.com/eolinker/go-common/pm3" +) + +func (p *plugin) apiApis() []pm3.Api { + return []pm3.Api{ + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/apis", []string{"context", "query:keyword", "query:service"}, []string{"apis"}, p.apiController.Search), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/apis/simple", []string{"context", "query:keyword", "query:service"}, []string{"apis"}, p.apiController.SimpleSearch), + //pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/simple/service/apis", []string{"context", "query:service"}, []string{"apis"}, p.apiController.SimpleList), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/api/detail", []string{"context", "query:service", "query:api"}, []string{"api"}, p.apiController.Detail), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/api/detail/simple", []string{"context", "query:service", "query:api"}, []string{"api"}, p.apiController.SimpleDetail), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/service/api", []string{"context", "query:service", "body"}, []string{"api"}, p.apiController.Create), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/service/api", []string{"context", "query:service", "query:api", "body"}, []string{"api"}, p.apiController.Edit), + pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/service/api", []string{"context", "query:service", "query:api"}, nil, p.apiController.Delete), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/service/api/copy", []string{"context", "query:service", "query:api", "body"}, []string{"api"}, p.apiController.Copy), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/api/doc", []string{"context", "query:service", "query:api"}, []string{"api"}, p.apiController.ApiDocDetail), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/api/proxy", []string{"context", "query:service", "query:api"}, []string{"api"}, p.apiController.ApiProxyDetail), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/api/define", []string{"context", "query:service"}, []string{"prefix", "force"}, p.apiController.Prefix), + } +} diff --git a/plugins/core/certificate.go b/plugins/core/certificate.go new file mode 100644 index 00000000..cb3fa5eb --- /dev/null +++ b/plugins/core/certificate.go @@ -0,0 +1,17 @@ +package core + +import ( + "net/http" + + "github.com/eolinker/go-common/pm3" +) + +func (p *plugin) certificateApi() []pm3.Api { + return []pm3.Api{ + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/certificates", []string{"context"}, []string{"certificates"}, p.certificateController.ListForPartition), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/certificate", []string{"context", "query:id"}, []string{"certificate", "cert"}, p.certificateController.Detail), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/certificate", []string{"context", "body"}, nil, p.certificateController.Create), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/certificate", []string{"context", "query:id", "body"}, nil, p.certificateController.Update), + pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/certificate", []string{"context", "query:id"}, []string{"id"}, p.certificateController.Delete), + } +} diff --git a/plugins/core/cluster.go b/plugins/core/cluster.go new file mode 100644 index 00000000..7708b264 --- /dev/null +++ b/plugins/core/cluster.go @@ -0,0 +1,15 @@ +package core + +import ( + "net/http" + + "github.com/eolinker/go-common/pm3" +) + +func (p *plugin) clusterApi() []pm3.Api { + return []pm3.Api{ + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/cluster/nodes", []string{"context", "query:partition"}, []string{"nodes"}, p.clusterController.Nodes), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/cluster/reset", []string{"context", "query:partition", "body"}, []string{"nodes"}, p.clusterController.ResetCluster), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/cluster/check", []string{"context", "body"}, []string{"nodes"}, p.clusterController.Check), + } +} diff --git a/plugins/core/common.go b/plugins/core/common.go new file mode 100644 index 00000000..c6f99acb --- /dev/null +++ b/plugins/core/common.go @@ -0,0 +1,14 @@ +package core + +import ( + "github.com/eolinker/go-common/ignore" + "github.com/eolinker/go-common/pm3" + "net/http" +) + +func (p *plugin) commonApis() []pm3.Api { + ignore.IgnorePath("login", http.MethodGet, "/api/v1/common/version") + return []pm3.Api{ + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/common/version", []string{"context"}, []string{"version", "build_time"}, p.commonController.Version), + } +} diff --git a/plugins/core/core.go b/plugins/core/core.go new file mode 100644 index 00000000..55c97a16 --- /dev/null +++ b/plugins/core/core.go @@ -0,0 +1,107 @@ +package core + +import ( + "net/http" + + plugin_cluster "github.com/APIParkLab/APIPark/controller/plugin-cluster" + + "github.com/APIParkLab/APIPark/controller/cluster" + + "github.com/eolinker/ap-account/controller/role" + + "github.com/APIParkLab/APIPark/controller/common" + + dynamic_module "github.com/APIParkLab/APIPark/controller/dynamic-module" + + "github.com/APIParkLab/APIPark/controller/release" + + application_authorization "github.com/APIParkLab/APIPark/controller/application-authorization" + + "github.com/APIParkLab/APIPark/controller/subscribe" + + "github.com/APIParkLab/APIPark/controller/api" + + "github.com/APIParkLab/APIPark/controller/upstream" + + "github.com/APIParkLab/APIPark/controller/service" + + "github.com/APIParkLab/APIPark/controller/catalogue" + + "github.com/APIParkLab/APIPark/controller/my_team" + + "github.com/APIParkLab/APIPark/controller/certificate" + "github.com/APIParkLab/APIPark/controller/team_manager" + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/pm3" +) + +func init() { + pm3.Register("core", new(Driver)) +} + +type Driver struct { +} + +func (d *Driver) Access() map[string][]string { + return map[string][]string{} +} + +func (d *Driver) Create() (pm3.IPlugin, error) { + + p := new(plugin) + autowire.Autowired(p) + return p, nil +} + +type plugin struct { + clusterController cluster.IClusterController `autowired:""` + certificateController certificate.ICertificateController `autowired:""` + teamManagerController team_manager.ITeamManagerController `autowired:""` + myTeamController my_team.ITeamController `autowired:""` + appController service.IAppController `autowired:""` + serviceController service.IServiceController `autowired:""` + //serviceController service.IServiceController `autowired:""` + catalogueController catalogue.ICatalogueController `autowired:""` + upstreamController upstream.IUpstreamController `autowired:""` + apiController api.IAPIController `autowired:""` + subscribeController subscribe.ISubscribeController `autowired:""` + appAuthorizationController application_authorization.IAuthorizationController `autowired:""` + releaseController release.IReleaseController `autowired:""` + roleController role.IRoleController `autowired:""` + subscribeApprovalController subscribe.ISubscribeApprovalController `autowired:""` + dynamicModuleController dynamic_module.IDynamicModuleController `autowired:""` + pluginClusterController plugin_cluster.IPluginClusterController `autowired:""` + commonController common.ICommonController `autowired:""` + apis []pm3.Api +} + +func (p *plugin) OnComplete() { + p.apis = append(p.apis, p.partitionApi()...) + p.apis = append(p.apis, p.certificateApi()...) + p.apis = append(p.apis, p.clusterApi()...) + p.apis = append(p.apis, p.TeamManagerApi()...) + p.apis = append(p.apis, p.MyTeamApi()...) + p.apis = append(p.apis, p.ServiceApis()...) + p.apis = append(p.apis, p.catalogueApi()...) + p.apis = append(p.apis, p.upstreamApis()...) + p.apis = append(p.apis, p.apiApis()...) + p.apis = append(p.apis, p.subscribeApis()...) + p.apis = append(p.apis, p.projectAuthorizationApis()...) + p.apis = append(p.apis, p.releaseApis()...) + p.apis = append(p.apis, p.DynamicModuleApis()...) + + p.apis = append(p.apis, p.PartitionPluginApi()...) + p.apis = append(p.apis, p.commonApis()...) +} + +func (p *plugin) Name() string { + return "core" +} + +func (p *plugin) APis() []pm3.Api { + return p.apis +} + +func (p *plugin) Assets() map[string]http.FileSystem { + return nil +} diff --git a/plugins/core/dynamic-module.go b/plugins/core/dynamic-module.go new file mode 100644 index 00000000..a37cdadc --- /dev/null +++ b/plugins/core/dynamic-module.go @@ -0,0 +1,21 @@ +package core + +import ( + "net/http" + + "github.com/eolinker/go-common/pm3" +) + +func (p *plugin) DynamicModuleApis() []pm3.Api { + return []pm3.Api{ + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/dynamic/:name/info", []string{"context", "rest:name", "query:id"}, []string{"info"}, p.dynamicModuleController.Get), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/dynamic/:name/list", []string{"context", "rest:name", "query:keyword", "query:partitions", "query:page", "query:pageSize"}, []string{"list", "basic", "total"}, p.dynamicModuleController.List), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/dynamic/:name", []string{"context", "rest:name", "body"}, []string{"info"}, p.dynamicModuleController.Create), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/dynamic/:name/config", []string{"context", "rest:name", "query:id", "body"}, []string{"info"}, p.dynamicModuleController.Edit), + pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/dynamic/:name/batch", []string{"context", "rest:name", "query:ids"}, nil, p.dynamicModuleController.Delete), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/dynamic/:name/render", []string{"context", "rest:name"}, []string{"basic", "render"}, p.dynamicModuleController.Render), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/dynamics/:group", []string{"context", "rest:group"}, []string{"dynamics"}, p.dynamicModuleController.ModuleDrivers), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/dynamic/:name/online", []string{"context", "rest:name", "query:id", "body"}, nil, p.dynamicModuleController.Online), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/dynamic/:name/offline", []string{"context", "rest:name", "query:id", "body"}, nil, p.dynamicModuleController.Offline), + } +} diff --git a/plugins/core/my-team.go b/plugins/core/my-team.go new file mode 100644 index 00000000..a21ea05b --- /dev/null +++ b/plugins/core/my-team.go @@ -0,0 +1,28 @@ +package core + +import ( + "net/http" + + "github.com/eolinker/go-common/pm3" +) + +func (p *plugin) MyTeamApi() []pm3.Api { + return []pm3.Api{ + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/team", []string{"context", "query:team"}, []string{"team"}, p.myTeamController.GetTeam), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/teams", []string{"context", "query:keyword"}, []string{"teams"}, p.myTeamController.Search), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/teams/mine", []string{"context", "query:keyword"}, []string{"teams"}, p.myTeamController.SimpleTeams), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/team/members/simple", []string{"context", "query:team", "query:keyword"}, []string{"teams"}, p.myTeamController.SimpleMembers), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/team", []string{"context", "query:team", "body"}, []string{"team"}, p.myTeamController.EditTeam), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/team/member", []string{"context", "query:team", "body"}, nil, p.myTeamController.AddMember), + pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/team/member", []string{"context", "query:team", "query:user"}, nil, p.myTeamController.RemoveMember), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/team/members", []string{"context", "query:team", "query:keyword"}, []string{"members"}, p.myTeamController.Members), + + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/team/member/role", []string{"context", "query:team", "body"}, nil, p.myTeamController.UpdateMemberRole), + + // 团队项目操作 + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/team/services", []string{"context", "query:team", "query:keyword"}, []string{"services"}, p.serviceController.Search), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/team/service", []string{"context", "query:team", "body"}, []string{"service"}, p.serviceController.Create), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/team/app", []string{"context", "query:team", "body"}, []string{"app"}, p.appController.CreateApp), + pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/team/service", []string{"context", "query:service"}, nil, p.serviceController.Delete), + } +} diff --git a/plugins/core/partition.go b/plugins/core/partition.go new file mode 100644 index 00000000..a1a663e8 --- /dev/null +++ b/plugins/core/partition.go @@ -0,0 +1,17 @@ +package core + +import ( + "github.com/eolinker/go-common/pm3" +) + +func (p *plugin) partitionApi() []pm3.Api { + return []pm3.Api{ + //pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/partitions", []string{"context", "query:keyword"}, []string{"partitions"}, p.partitionController.Search), + //pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/partition", []string{"context", "query:id"}, []string{"partition"}, p.partitionController.Info), + //pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/partition", []string{"context", "body"}, []string{"partition", "id", "update_time"}, p.partitionController.Create), + //pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/partition", []string{"context", "query:id", "body"}, []string{"partition"}, p.partitionController.Update), + //pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/partition", []string{"context", "query:id"}, []string{"id"}, p.partitionController.Delete), + //pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/partitions", []string{"context"}, []string{"partitions"}, p.partitionController.Simple), + //pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/partitions/cluster", []string{"context"}, []string{"partitions"}, p.partitionController.SimpleWithCluster), + } +} diff --git a/plugins/core/plugin.go b/plugins/core/plugin.go new file mode 100644 index 00000000..42e9366c --- /dev/null +++ b/plugins/core/plugin.go @@ -0,0 +1,17 @@ +package core + +import ( + "net/http" + + "github.com/eolinker/go-common/pm3" +) + +func (p *plugin) PartitionPluginApi() []pm3.Api { + return []pm3.Api{ + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/apinto/plugin/:plugin", []string{"context", "query:partition", "rest:plugin"}, []string{"plugin", "render"}, p.pluginClusterController.Get), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/apinto/plugins", []string{"context", "query:partition"}, []string{"plugins"}, p.pluginClusterController.List), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/apinto/plugins/project", []string{"context", "query:project"}, []string{"plugins"}, p.pluginClusterController.Option), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/apinto/plugin/:name", []string{"context", "rest:name"}, []string{"plugins"}, p.pluginClusterController.Info), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/apinto/plugin/:plugin", []string{"context", "query:partition", "rest:plugin", "body"}, []string{}, p.pluginClusterController.Set), + } +} diff --git a/plugins/core/project-authorization.go b/plugins/core/project-authorization.go new file mode 100644 index 00000000..e2ad3db7 --- /dev/null +++ b/plugins/core/project-authorization.go @@ -0,0 +1,18 @@ +package core + +import ( + "net/http" + + "github.com/eolinker/go-common/pm3" +) + +func (p *plugin) projectAuthorizationApis() []pm3.Api { + return []pm3.Api{ + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/app/authorization", []string{"context", "query:app", "body"}, []string{"authorization"}, p.appAuthorizationController.AddAuthorization), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/app/authorization", []string{"context", "query:app", "query:authorization", "body"}, []string{"authorization"}, p.appAuthorizationController.EditAuthorization), + pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/app/authorization", []string{"context", "query:app", "query:authorization"}, nil, p.appAuthorizationController.DeleteAuthorization), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/app/authorization", []string{"context", "query:app", "query:authorization"}, []string{"authorization"}, p.appAuthorizationController.Info), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/app/authorizations", []string{"context", "query:app"}, []string{"authorizations"}, p.appAuthorizationController.Authorizations), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/app/authorization/details", []string{"context", "query:app", "query:authorization"}, []string{"details"}, p.appAuthorizationController.Detail), + } +} diff --git a/plugins/core/release.go b/plugins/core/release.go new file mode 100644 index 00000000..750d6bfd --- /dev/null +++ b/plugins/core/release.go @@ -0,0 +1,18 @@ +package core + +import ( + "net/http" + + "github.com/eolinker/go-common/pm3" +) + +func (p *plugin) releaseApis() []pm3.Api { + return []pm3.Api{ + + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/service/release", []string{"context", "query:service", "body"}, []string{}, p.releaseController.Create), + pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/service/release", []string{"context", "query:service", "query:id"}, []string{}, p.releaseController.Delete), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/release", []string{"context", "query:service", "query:id"}, []string{"release"}, p.releaseController.Detail), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/releases", []string{"context", "query:service"}, []string{"releases"}, p.releaseController.List), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/release/preview", []string{"context", "query:service"}, []string{"running", "diff", "complete"}, p.releaseController.Preview), + } +} diff --git a/plugins/core/service.go b/plugins/core/service.go new file mode 100644 index 00000000..4d3f8c8a --- /dev/null +++ b/plugins/core/service.go @@ -0,0 +1,34 @@ +package core + +import ( + "net/http" + + "github.com/eolinker/go-common/pm3" +) + +func (p *plugin) ServiceApis() []pm3.Api { + return []pm3.Api{ + // 项目 + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/info", []string{"context", "query:service"}, []string{"service"}, p.serviceController.Get), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/service/info", []string{"context", "query:service", "body"}, []string{"service"}, p.serviceController.Edit), + pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/service/info", []string{"context", "query:service"}, nil, p.serviceController.Delete), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/my_services", []string{"context", "query:team", "query:keyword"}, []string{"services"}, p.serviceController.SearchMyServices), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/services", []string{"context", "query:team", "query:keyword"}, []string{"services"}, p.serviceController.Search), + + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/services/mine", []string{"context", "query:keyword"}, []string{"services"}, p.serviceController.MySimple), + + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/services", []string{"context", "query:keyword"}, []string{"services"}, p.serviceController.Simple), + + // 应用相关 + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/app/info", []string{"context", "query:app"}, []string{"app"}, p.appController.GetApp), + pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/app", []string{"context", "query:app"}, nil, p.appController.DeleteApp), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/apps", []string{"context", "query:keyword"}, []string{"apps"}, p.appController.SimpleApps), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/simple/apps/mine", []string{"context", "query:keyword"}, []string{"apps"}, p.appController.MySimpleApps), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/my_apps", []string{"context", "query:team", "query:keyword"}, []string{"apps"}, p.appController.SearchMyApps), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/apps", []string{"context", "query:team", "query:keyword"}, []string{"apps"}, p.appController.Search), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/app/info", []string{"context", "query:app", "body"}, []string{"app"}, p.appController.UpdateApp), + + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/doc", []string{"context", "query:service"}, []string{"doc"}, p.serviceController.ServiceDoc), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/service/doc", []string{"context", "query:service", "body"}, nil, p.serviceController.SaveServiceDoc), + } +} diff --git a/plugins/core/square.go b/plugins/core/square.go new file mode 100644 index 00000000..486858ef --- /dev/null +++ b/plugins/core/square.go @@ -0,0 +1,20 @@ +package core + +import ( + "net/http" + + "github.com/eolinker/go-common/pm3" +) + +func (p *plugin) catalogueApi() []pm3.Api { + return []pm3.Api{ + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/catalogues", []string{"context", "query:keyword"}, []string{"catalogues", "tags"}, p.catalogueController.Search), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/catalogue", []string{"context", "body"}, nil, p.catalogueController.Create), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/catalogue", []string{"context", "query:catalogue", "body"}, nil, p.catalogueController.Edit), + pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/catalogue", []string{"context", "query:catalogue"}, nil, p.catalogueController.Delete), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/catalogue/sort", []string{"context", "body"}, nil, p.catalogueController.Sort), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/catalogue/services", []string{"context", "query:keyword"}, []string{"services"}, p.catalogueController.Services), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/catalogue/service", []string{"context", "query:service"}, []string{"service"}, p.catalogueController.ServiceDetail), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/catalogue/service/subscribe", []string{"context", "body"}, nil, p.catalogueController.Subscribe), + } +} diff --git a/plugins/core/subscribe.go b/plugins/core/subscribe.go new file mode 100644 index 00000000..8a0d7a7e --- /dev/null +++ b/plugins/core/subscribe.go @@ -0,0 +1,25 @@ +package core + +import ( + "net/http" + + "github.com/eolinker/go-common/pm3" +) + +func (p *plugin) subscribeApis() []pm3.Api { + return []pm3.Api{ + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/subscribers", []string{"context", "query:service", "query:keyword"}, []string{"subscribers"}, p.subscribeController.Search), + + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/service/subscriber", []string{"context", "query:service", "body"}, nil, p.subscribeController.AddSubscriber), + pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/service/subscriber", []string{"context", "query:service", "query:service", "query:application"}, nil, p.subscribeController.DeleteSubscriber), + + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/application/subscriptions", []string{"context", "query:application", "query:keyword"}, []string{"subscriptions"}, p.subscribeController.SearchSubscriptions), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/application/subscription/cancel", []string{"context", "query:application", "query:subscription"}, nil, p.subscribeController.RevokeSubscription), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/application/subscription/cancel_apply", []string{"context", "query:application", "query:subscription"}, nil, p.subscribeController.RevokeApply), + + // 审批相关 + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/approval/subscribes", []string{"context", "query:service", "query:status"}, []string{"approvals"}, p.subscribeApprovalController.GetApprovalList), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/approval/subscribe", []string{"context", "query:service", "query:apply"}, []string{"approval"}, p.subscribeApprovalController.GetApprovalDetail), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/service/approval/subscribe", []string{"context", "query:service", "query:apply", "body"}, nil, p.subscribeApprovalController.Approval), + } +} diff --git a/plugins/core/team-manager.go b/plugins/core/team-manager.go new file mode 100644 index 00000000..7c03c9d2 --- /dev/null +++ b/plugins/core/team-manager.go @@ -0,0 +1,16 @@ +package core + +import ( + "github.com/eolinker/go-common/pm3" + "net/http" +) + +func (p *plugin) TeamManagerApi() []pm3.Api { + return []pm3.Api{ + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/manager/team", []string{"context", "query:id"}, []string{"team"}, p.teamManagerController.GetTeam), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/manager/teams", []string{"context", "query:keyword"}, []string{"teams"}, p.teamManagerController.Search), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/manager/team", []string{"context", "body"}, []string{"team"}, p.teamManagerController.CreateTeam), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/manager/team", []string{"context", "query:id", "body"}, []string{"team"}, p.teamManagerController.EditTeam), + pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/manager/team", []string{"context", "query:id"}, []string{"id"}, p.teamManagerController.DeleteTeam), + } +} diff --git a/plugins/core/upstream.go b/plugins/core/upstream.go new file mode 100644 index 00000000..af77a869 --- /dev/null +++ b/plugins/core/upstream.go @@ -0,0 +1,14 @@ +package core + +import ( + "net/http" + + "github.com/eolinker/go-common/pm3" +) + +func (p *plugin) upstreamApis() []pm3.Api { + return []pm3.Api{ + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/upstream", []string{"context", "query:service"}, []string{"upstream"}, p.upstreamController.Get), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/service/upstream", []string{"context", "query:service", "body"}, []string{"upstream"}, p.upstreamController.Save), + } +} diff --git a/plugins/permit/driver.go b/plugins/permit/driver.go new file mode 100644 index 00000000..2a6f70eb --- /dev/null +++ b/plugins/permit/driver.go @@ -0,0 +1,23 @@ +package permit + +import ( + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/pm3" +) + +var ( + _ pm3.Driver = (*Driver)(nil) +) + +func init() { + pm3.Register("permit", &Driver{}) +} + +type Driver struct { +} + +func (d *Driver) Create() (pm3.IPlugin, error) { + p := new(pluginPermit) + autowire.Autowired(p) + return p, nil +} diff --git a/plugins/permit/plugin.go b/plugins/permit/plugin.go new file mode 100644 index 00000000..4ad20607 --- /dev/null +++ b/plugins/permit/plugin.go @@ -0,0 +1,41 @@ +package permit + +import ( + "github.com/APIParkLab/APIPark/controller/permit_system" + "github.com/APIParkLab/APIPark/controller/permit_team" + permit_middleware "github.com/APIParkLab/APIPark/middleware/permit" + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/pm3" +) + +var ( + _ pm3.IPluginApis = (*pluginPermit)(nil) + _ pm3.IPluginMiddleware = (*pluginPermit)(nil) + _ autowire.Complete = (*pluginPermit)(nil) +) + +type pluginPermit struct { + systemPermitController permit_system.ISystemPermitController `autowired:""` + teamPermitController permit_team.ITeamPermitController `autowired:""` + apis []pm3.Api + middlewares []pm3.IMiddleware + permitChecker permit_middleware.IPermitMiddleware `autowired:""` +} + +func (p *pluginPermit) OnComplete() { + p.apis = append(p.apis, p.getSystemApis()...) + p.apis = append(p.apis, p.getSTeamPermitApis()...) + p.middlewares = append(p.middlewares, p.permitChecker) +} + +func (p *pluginPermit) APis() []pm3.Api { + return p.apis +} + +func (p *pluginPermit) Middlewares() []pm3.IMiddleware { + return p.middlewares +} + +func (p *pluginPermit) Name() string { + return "permit" +} diff --git a/plugins/permit/system.go b/plugins/permit/system.go new file mode 100644 index 00000000..69a4c578 --- /dev/null +++ b/plugins/permit/system.go @@ -0,0 +1,13 @@ +package permit + +import ( + "net/http" + + "github.com/eolinker/go-common/pm3" +) + +func (p *pluginPermit) getSystemApis() []pm3.Api { + return []pm3.Api{ + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/profile/permission/system", []string{"context"}, []string{"access"}, p.systemPermitController.Permissions), + } +} diff --git a/plugins/permit/team.go b/plugins/permit/team.go new file mode 100644 index 00000000..729e2aab --- /dev/null +++ b/plugins/permit/team.go @@ -0,0 +1,13 @@ +package permit + +import ( + "net/http" + + "github.com/eolinker/go-common/pm3" +) + +func (p *pluginPermit) getSTeamPermitApis() []pm3.Api { + return []pm3.Api{ + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/profile/permission/team", []string{"context", "query:team"}, []string{"access"}, p.teamPermitController.Permissions), + } +} diff --git a/plugins/plugins.go b/plugins/plugins.go new file mode 100644 index 00000000..da4c664e --- /dev/null +++ b/plugins/plugins.go @@ -0,0 +1,2 @@ +// 封装为插件 +package plugins diff --git a/plugins/publish_flow/apis.go b/plugins/publish_flow/apis.go new file mode 100644 index 00000000..d02819a8 --- /dev/null +++ b/plugins/publish_flow/apis.go @@ -0,0 +1,24 @@ +package publish_flow + +import ( + "net/http" + + "github.com/eolinker/go-common/pm3" +) + +func (p *plugin) getApis() []pm3.Api { + return []pm3.Api{ + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/service/publish/release", []string{"context", "query:service", "body"}, []string{"publish"}, p.controller.ApplyOnRelease), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/service/publish/release/do", []string{"context", "query:service", "body"}, []string{"publish"}, p.controller.ReleaseDo), + pm3.CreateApiWidthDoc(http.MethodPost, "/api/v1/service/publish/apply", []string{"context", "query:service", "body"}, []string{"publish"}, p.controller.Apply), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/publishs", []string{"context", "query:service", "query:page", "query:page_size"}, []string{"publishs", "page", "size", "total"}, p.controller.ListPage), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/publish", []string{"context", "query:service", "query:id"}, []string{"publish"}, p.controller.Detail), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/publish/check", []string{"context", "query:service", "query:release"}, []string{"diffs"}, p.controller.CheckPublish), + pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/service/publish/close", []string{"context", "query:service", "query:id"}, []string{}, p.controller.Close), + pm3.CreateApiWidthDoc(http.MethodDelete, "/api/v1/service/publish/stop", []string{"context", "query:service", "query:id"}, []string{}, p.controller.Stop), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/service/publish/refuse", []string{"context", "query:service", "query:id", "body"}, []string{}, p.controller.Refuse), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/service/publish/accept", []string{"context", "query:service", "query:id", "body"}, []string{}, p.controller.Accept), + pm3.CreateApiWidthDoc(http.MethodPut, "/api/v1/service/publish/execute", []string{"context", "query:service", "query:id"}, []string{}, p.controller.Publish), + pm3.CreateApiWidthDoc(http.MethodGet, "/api/v1/service/publish/status", []string{"context", "query:service", "query:id"}, []string{"publish_status_list"}, p.controller.PublishStatuses), + } +} diff --git a/plugins/publish_flow/driver.go b/plugins/publish_flow/driver.go new file mode 100644 index 00000000..03f59774 --- /dev/null +++ b/plugins/publish_flow/driver.go @@ -0,0 +1,19 @@ +package publish_flow + +import ( + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/pm3" +) + +type Driver struct { +} + +func (d *Driver) Create() (pm3.IPlugin, error) { + p := new(plugin) + autowire.Autowired(p) + return p, nil +} + +func init() { + pm3.Register("publish_flow", new(Driver)) +} diff --git a/plugins/publish_flow/plugin.go b/plugins/publish_flow/plugin.go new file mode 100644 index 00000000..c8c6e4bf --- /dev/null +++ b/plugins/publish_flow/plugin.go @@ -0,0 +1,29 @@ +package publish_flow + +import ( + "github.com/APIParkLab/APIPark/controller/publish" + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/pm3" +) + +var ( + _ pm3.IPluginApis = (*plugin)(nil) + _ autowire.Complete = (*plugin)(nil) +) + +type plugin struct { + controller publish.IPublishController `autowired:""` + apis []pm3.Api +} + +func (p *plugin) Name() string { + return "publish_flow" +} + +func (p *plugin) OnComplete() { + p.apis = append(p.apis, p.getApis()...) +} + +func (p *plugin) APis() []pm3.Api { + return p.apis +} diff --git a/resources/access/access.go b/resources/access/access.go new file mode 100644 index 00000000..25c38843 --- /dev/null +++ b/resources/access/access.go @@ -0,0 +1,45 @@ +package access + +import ( + _ "embed" + + "github.com/eolinker/go-common/access" + "gopkg.in/yaml.v3" +) + +type Access = access.Access + +var ( + //go:embed access.yaml + data []byte +) + +func init() { + ts := make(map[string][]Access) + err := yaml.Unmarshal(data, &ts) + if err != nil { + panic(err) + } + for group, asl := range ts { + access.Add(group, asl) + + } + //defaultRoles := access.Roles() + //for group, rs := range defaultRoles { + // p, has := access.GetPermit(group) + // if !has { + // continue + // } + // + // for _, r := range rs { + // for _, pm := range r.Permits { + // apis, err := p.GetPermits(pm) + // if err != nil { + // continue + // } + // permit.AddPermitRule(pm, apis...) + // } + // } + // + //} +} diff --git a/resources/access/access.yaml b/resources/access/access.yaml new file mode 100644 index 00000000..fbed5b8b --- /dev/null +++ b/resources/access/access.yaml @@ -0,0 +1,368 @@ +system: + - name: organization + cname: '组织管理' + value: 'organization' + children: + - name: member + cname: '成员' + value: 'member' + children: + - name: view + cname: '查看' + value: 'view' + apis: + - "GET:/api/v1/user/accounts" + - "GET:/api/v1/user/departments" + - name: manager + cname: '管理' + value: 'manager' + apis: + - "POST:/api/v1/user/account" + - "PUT:/api/v1/user/account" + - "DELETE:/api/v1/user/account" + - "POST:/api/v1/user/account/enable" + - "POST:/api/v1/user/account/disable" + - "POST:/api/v1/user/department" + - "PUT:/api/v1/user/department" + - "DELETE:/api/v1/user/department" + - "POST:/api/v1/user/department/member" + - "DELETE:/api/v1/user/department/member" + - "POST:/api/v1/user/department/member/remove" + - "POST:/api/v1/account/role" + dependents: + - system.organization.member.view + - name: team + cname: '团队' + value: 'team' + children: + - name: view + cname: '查看' + value: 'view' + apis: + - "GET:/api/v1/manager/teams" + - "GET:/api/v1/manager/team" + - name: manager + cname: '管理' + value: 'manager' + apis: + - "POST:/api/v1/manager/team" + - "PUT:/api/v1/manager/team" + - "DELETE:/api/v1/manager/team" + dependents: + - system.organization.team.view + - name: role + cname: '角色' + value: 'role' + children: + - name: view system role + cname: '查看系统角色' + value: 'view_system_role' + apis: + - "GET:/api/v1/system/roles" + - "GET:/api/v1/system/role" + - name: view team role + cname: '查看团队角色' + value: 'view_team_role' + apis: + - "GET:/api/v1/team/roles" + - "GET:/api/v1/team/role" + - name: API Market + cname: 'API市场' + value: 'api_market' + children: + - name: service classification + cname: '服务分类' + value: 'service_classification' + children: + - name: view + cname: '查看' + value: 'view' +# apis: +# - "GET:/api/v1/catalogues" + - name: manager + cname: '管理' + value: 'manager' + apis: + - "POST:/api/v1/catalogue" + - "PUT:/api/v1/catalogue" + - "DELETE:/api/v1/catalogue" + - "PUT:/api/v1/catalogue/sort" + dependents: + - system.api_market.service_classification.view + - name: devops + cname: 运维 + value: 'devops' + children: + - name: cluster + cname: 集群 + value: 'cluster' + children: + - name: view + cname: 查看 + value: 'view' + apis: + - "GET:/api/v1/cluster/nodes" + - name: manager + cname: 管理 + value: 'manager' + apis: + - "PUT:/api/v1/cluster/reset" + - "POST:/api/v1/cluster/check" + - name: ssl certificate + cname: 证书 + value: 'ssl_certificate' + children: + - name: view + cname: 查看 + value: 'view' + apis: + - "GET:/api/v1/certificates" + - "GET:/api/v1/certificate" + - name: manager + cname: 管理 + value: 'manager' + apis: + - "POST:/api/v1/certificate" + - "PUT:/api/v1/certificate" + - "DELETE:/api/v1/certificate" + + - name: log configuration + cname: 日志 + value: 'log_configuration' + children: + - name: view + cname: 查看 + value: 'view' + apis: + - "GET:/api/v1/dynamic/{name}/info" + - "GET:/api/v1/dynamic/{name}/list" + - "GET:/api/v1/dynamic/{name}/render" + - name: manager + cname: 管理 + value: 'manager' + apis: + - "POST:/api/v1/dynamic/{name}" + - "PUT:/api/v1/dynamic/{name}/config" + - "DELETE:/api/v1/dynamic/{name}/batch" + - "PUT:/api/v1/dynamic/{name}/online" + - "PUT:/api/v1/dynamic/{name}/offline" + - name: workspace + cname: 工作空间 + value: 'workspace' + children: + - name: application + cname: 应用 + value: 'application' + children: + - name: view all + cname: 查看所有应用 + value: 'view_all' + apis: + - "GET:/api/v1/apps" + - name: service + cname: 服务 + value: 'service' + children: + - name: view all + cname: 查看所有服务 + value: 'view_all' + apis: + - "GET:/api/v1/services" + - name: team + cname: 团队 + value: 'team' + children: + - name: view all + cname: 查看所有团队 + value: 'view_all' + apis: + - "GET:/api/v1/manager/teams" + - name: api market + cname: API市场 + value: 'api_market' + children: + - name: view + cname: 查看 + value: 'view' + apis: + - "GET:/api/v1/catalogue/services" + - "GET:/api/v1/catalogue/service" +team: + - name: service + cname: 服务 + value: 'service' + children: + - name: api + cname: API + value: 'api' + children: + - name: view + cname: 查看 + value: 'view' + apis: + - "GET:/api/v1/service/apis" + - "GET:/api/v1/service/api/detail" + - "GET:/api/v1/service/api/detail/simple" + - "GET:/api/v1/service/api/define" + - "GET:/api/v1/service/apis/simple" + - name: manager + cname: 管理 + value: 'manager' + apis: + - "POST:/api/v1/service/api" + - "PUT:/api/v1/service/api" + - "DELETE:/api/v1/service/api" + - "POST:/api/v1/service/api/copy" + - name: upstream + cname: 上游 + value: 'upstream' + children: + - name: view + cname: 查看 + value: 'view' + apis: + - "GET:/api/v1/service/upstream" + - name: manager + cname: 管理 + value: 'manager' + apis: + - "PUT:/api/v1/service/upstream" + - name: release + cname: 发布 + value: 'release' + children: + - name: view + cname: 查看 + value: 'view' + apis: + - "GET:/api/v1/service/releases" + - "GET:/api/v1/service/release" + - "GET:/api/v1/service/publishs" + - "GET:/api/v1/service/publish/check" + - "GET:/api/v1/service/release/preview" + - "GET:/api/v1/service/publish/status" + - name: manager + cname: 管理 + value: 'manager' + apis: + - "POST:/api/v1/service/publish/release/do" + - "PUT:/api/v1/service/publish/execute" + - "DELETE:/api/v1/service/release" + - name: subscription management + cname: 订阅方管理 + value: 'subscription' + children: + - name: view + cname: 查看 + value: 'view' + apis: + - "GET:/api/v1/service/approval/subscribes" + - "GET:/api/v1/service/approval/subscribe" + - "GET:/api/v1/service/subscribers" + - name: manager + cname: 管理 + value: 'manager' + apis: + - "POST:/api/v1/service/approval/subscribe" + - "POST:/api/v1/service/subscriber" + - "DELETE:/api/v1/service/subscriber" + - name: service + cname: 服务管理 + value: 'service' + children: + - name: manager + cname: 管理 + value: 'manager' + apis: + - "GET:/api/v1/service/info" + - "PUT:/api/v1/service/info" + - "POST:/api/v1/team/service" + - "DELETE:/api/v1/team/service" + - name: application + cname: 应用 + value: 'application' + children: + - name: subscription Service + cname: 订阅服务 + value: 'subscription' + children: + - name: view + cname: 查看 + value: 'view' + apis: + - "GET:/api/v1/application/subscriptions" + - name: manager + cname: 管理 + value: 'manager' + apis: + - "POST:/api/v1/catalogue/service/subscribe" + - "POST:/api/v1/application/subscription/cancel" + - "POST:/api/v1/application/subscription/cancel_apply" + - name: authorization + cname: 访问授权 + value: 'authorization' + children: + - name: view + cname: 查看 + value: 'view' + apis: + - "GET:/api/v1/app/authorization" + - "GET:/api/v1/app/authorizations" + - "GET:/api/v1/app/authorization/details" + - name: manager + cname: 管理 + value: 'manager' + apis: + - "POST:/api/v1/app/authorization" + - "PUT:/api/v1/app/authorization" + - "DELETE:/api/v1/app/authorization" + - name: application + cname: 应用 + value: 'application' + children: + - name: manager + cname: 管理 + value: 'manager' + apis: + - "GET:/api/v1/app/info" + - "PUT:/api/v1/app/info" + - "POST:/api/v1/team/app" + - "DELETE:/api/v1/app" + - name: team + cname: 团队 + value: 'team' + children: + - name: member + cname: 成员 + value: 'member' + children: + - name: view + cname: 查看 + value: 'view' + apis: + - "GET:/api/v1/team/members" + - "GET:/api/v1/team/members/toadd" + - name: manager + cname: 管理 + value: 'manager' + apis: + - "POST:/api/v1/team/member" + - "DELETE:/api/v1/team/member" + - "PUT:/api/v1/team/member/role" + - name: team + cname: 团队管理 + value: 'team' + children: + - name: view + cname: '查看' + value: 'view' + apis: + - "GET:/api/v1/manager/teams" + - "GET:/api/v1/manager/team" + - name: manager + cname: '管理' + value: 'manager' + apis: + - "POST:/api/v1/manager/team" + - "PUT:/api/v1/manager/team" + - "DELETE:/api/v1/manager/team" \ No newline at end of file diff --git a/resources/access/access_test.go b/resources/access/access_test.go new file mode 100644 index 00000000..95d216e7 --- /dev/null +++ b/resources/access/access_test.go @@ -0,0 +1,57 @@ +package access + +import ( + "fmt" + "os" + "sort" + "strings" + "testing" + + "github.com/eolinker/go-common/access" +) + +func TestPrintlnRoleAccess(t *testing.T) { + system, has := access.GetPermit("system") + if has { + keys := system.AccessKeys() + sort.Strings(keys) + for _, k := range keys { + fmt.Printf("- %s\n", k) + } + } + team, has := access.GetPermit("team") + if has { + keys := team.AccessKeys() + sort.Strings(keys) + for _, k := range keys { + fmt.Printf("- %s\n", k) + } + } +} + +func TestPrintlnAccessAPIs(t *testing.T) { + builder := &strings.Builder{} + printAccesses("system", builder) + printAccesses("team", builder) + os.WriteFile("permit.yaml", []byte(builder.String()), 0644) +} + +func printAccesses(group string, builder *strings.Builder) { + handler, has := access.GetPermit(group) + if has { + keys := handler.AccessKeys() + sort.Strings(keys) + builder.WriteString(fmt.Sprintf("%s:\n", group)) + for _, key := range keys { + apis, err := handler.GetPermits(key) + if err != nil { + continue + } + builder.WriteString(fmt.Sprintf(" %s:\n", key)) + for _, api := range apis { + builder.WriteString(fmt.Sprintf(" - %s\n", api)) + } + } + } + return +} diff --git a/resources/access/permit.yaml b/resources/access/permit.yaml new file mode 100644 index 00000000..971bfe5c --- /dev/null +++ b/resources/access/permit.yaml @@ -0,0 +1,142 @@ +system: + system.api_market.service_classification.manager: + - POST:/api/v1/catalogue + - PUT:/api/v1/catalogue + - DELETE:/api/v1/catalogue + - PUT:/api/v1/catalogue/sort + system.api_market.service_classification.view: + - GET:/api/v1/catalogues + system.devops.cluster.manager: + - PUT:/api/v1/cluster/reset + - POST:/api/v1/cluster/check + system.devops.cluster.view: + - GET:/api/v1/cluster/nodes + system.devops.log_configuration.manager: + - POST:/api/v1/dynamic/{name} + - PUT:/api/v1/dynamic/{name}/config + - DELETE:/api/v1/dynamic/{name}/batch + - PUT:/api/v1/dynamic/{name}/online + - PUT:/api/v1/dynamic/{name}/offline + system.devops.log_configuration.view: + - GET:/api/v1/dynamic/{name}/info + - GET:/api/v1/dynamic/{name}/list + - GET:/api/v1/dynamic/{name}/render + system.devops.ssl_certificate.manager: + - POST:/api/v1/certificate + - PUT:/api/v1/certificate + - DELETE:/api/v1/certificate + system.devops.ssl_certificate.view: + - GET:/api/v1/certificates + - GET:/api/v1/certificate + system.organization.member.manager: + - POST:/api/v1/user/account + - PUT:/api/v1/user/account + - DELETE:/api/v1/user/account + - POST:/api/v1/user/account/enable + - POST:/api/v1/user/account/disable + - POST:/api/v1/user/department + - PUT:/api/v1/user/department + - DELETE:/api/v1/user/department + - POST:/api/v1/user/department/member + - DELETE:/api/v1/user/department/member + - POST:/api/v1/user/department/member/remove + - POST:/api/v1/account/role + system.organization.member.view: + - GET:/api/v1/user/accounts + - GET:/api/v1/user/departments + system.organization.role.view_system_role: + - GET:/api/v1/system/roles + - GET:/api/v1/system/role + system.organization.role.view_team_role: + - GET:/api/v1/team/roles + - GET:/api/v1/team/role + system.organization.team.manager: + - POST:/api/v1/manager/team + - PUT:/api/v1/manager/team + - DELETE:/api/v1/manager/team + system.organization.team.view: + - GET:/api/v1/manager/teams + - GET:/api/v1/manager/team + system.workspace.api_market.view: + - GET:/api/v1/catalogue/services + - GET:/api/v1/catalogue/service + system.workspace.application.view_all: + - GET:/api/v1/apps + system.workspace.service.view_all: + - GET:/api/v1/services + system.workspace.team.view_all: + - GET:/api/v1/teams +team: + team.application.application.manager: + - GET:/api/v1/app/info + - PUT:/api/v1/app/info + - POST:/api/v1/team/app + - DELETE:/api/v1/app + team.application.authorization.manager: + - POST:/api/v1/app/authorization + - PUT:/api/v1/app/authorization + - DELETE:/api/v1/app/authorization + team.application.authorization.view: + - GET:/api/v1/app/authorization + - GET:/api/v1/app/authorizations + - GET:/api/v1/app/authorization/details + team.application.subscription.manager: + - POST:/api/v1/catalogue/service/subscribe + - POST:/api/v1/application/subscription/cancel + - POST:/api/v1/application/subscription/cancel_apply + team.application.subscription.view: + - GET:/api/v1/application/subscriptions + team.service.api.manager: + - POST:/api/v1/service/api + - PUT:/api/v1/service/api + - DELETE:/api/v1/service/api + - POST:/api/v1/service/api/copy + team.service.api.view: + - GET:/api/v1/service/apis + - GET:/api/v1/service/api/detail + - GET:/api/v1/service/api/detail/simple + - GET:/api/v1/service/api/define + - GET:/api/v1/service/apis/simple + team.service.release.manager: + - POST:/api/v1/service/publish/release/do + - PUT:/api/v1/service/publish/execute + - DELETE:/api/v1/service/release + team.service.release.view: + - GET:/api/v1/service/releases + - GET:/api/v1/service/release + - GET:/api/v1/service/publishs + - GET:/api/v1/service/publish/check + - GET:/api/v1/service/release/preview + - GET:/api/v1/service/publish/status + team.service.service.manager: + - GET:/api/v1/service/info + - PUT:/api/v1/service/info + - POST:/api/v1/team/service + - DELETE:/api/v1/team/service + team.service.subscription.manager: + - POST:/api/v1/service/approval/subscribe + - POST:/api/v1/service/subscriber + - DELETE:/api/v1/service/subscriber + team.service.subscription.view: + - GET:/api/v1/service/approval/subscribes + - GET:/api/v1/service/approval/subscribe + - GET:/api/v1/service/subscribers + team.service.upstream.manager: + - PUT:/api/v1/service/upstream + team.service.upstream.view: + - GET:/api/v1/service/upstream + team.team.member.manager: + - POST:/api/v1/team/member + - DELETE:/api/v1/team/member + - PUT:/api/v1/team/member/role + team.team.member.view: + - GET:/api/v1/team/members + - GET:/api/v1/team/members/simple + - GET:/api/v1/team/members/toadd + team.team.team.manager: + - POST:/api/v1/manager/team + - PUT:/api/v1/manager/team + - DELETE:/api/v1/manager/team + team.team.team.view: + - GET:/api/v1/manager/teams + - GET:/api/v1/manager/team diff --git a/resources/access/role.go b/resources/access/role.go new file mode 100644 index 00000000..4b1ba464 --- /dev/null +++ b/resources/access/role.go @@ -0,0 +1,24 @@ +package access + +import ( + _ "embed" + + "github.com/eolinker/go-common/access" + "gopkg.in/yaml.v3" +) + +type Role = access.Role + +var ( + //go:embed role.yaml + roleData []byte +) + +func init() { + ts := make(map[string][]Role) + err := yaml.Unmarshal(roleData, &ts) + if err != nil { + panic(err) + } + access.RoleAdd(ts) +} diff --git a/resources/access/role.yaml b/resources/access/role.yaml new file mode 100644 index 00000000..05d8898d --- /dev/null +++ b/resources/access/role.yaml @@ -0,0 +1,116 @@ +system: + - name: supper_admin + cname: 超级管理员 + permits: + - system.api_market.service_classification.manager + - system.api_market.service_classification.view + - system.devops.cluster.manager + - system.devops.cluster.view + - system.devops.log_configuration.manager + - system.devops.log_configuration.view + - system.devops.ssl_certificate.manager + - system.devops.ssl_certificate.view + - system.organization.member.manager + - system.organization.member.view + - system.organization.role.view_system_role + - system.organization.role.view_team_role + - system.organization.team.manager + - system.organization.team.view + - system.workspace.api_market.view + - system.workspace.application.view_all + - system.workspace.service.view_all + - system.workspace.team.view_all + supper: true + - name: team_admin + cname: 团队管理员 + permits: + - system.organization.role.view_team_role + - system.organization.team.manager + - system.organization.team.view + - system.workspace.api_market.view + - system.workspace.application.view_all + - system.workspace.service.view_all + - system.workspace.team.view_all + - name: devops_admin + cname: 运维管理员 + permits: + - system.api_market.service_classification.manager + - system.api_market.service_classification.view + - system.devops.cluster.manager + - system.devops.cluster.view + - system.devops.log_configuration.manager + - system.devops.log_configuration.view + - system.devops.ssl_certificate.manager + - system.devops.ssl_certificate.view + - system.workspace.api_market.view + - system.workspace.application.view_all + - system.workspace.service.view_all + - system.workspace.team.view_all + - name: member + cname: 普通成员 + permits: + - system.workspace.api_market.view + default: true +team: + - name: team_admin + cname: 团队管理员 + permits: + - team.application.application.manager + - team.application.authorization.manager + - team.application.authorization.view + - team.application.subscription.manager + - team.application.subscription.view + - team.service.api.manager + - team.service.api.view + - team.service.release.manager + - team.service.release.view + - team.service.service.manager + - team.service.subscription.manager + - team.service.subscription.view + - team.service.upstream.manager + - team.service.upstream.view + - team.team.member.manager + - team.team.member.view + - team.team.team.manager + - team.team.team.view + supper: true + - name: service_admin + cname: 服务管理员 + permits: + - team.service.service.manager + - team.service.upstream.manager + - team.service.upstream.view + - team.service.api.manager + - team.service.api.view + - team.service.subscription.manager + - team.service.subscription.view + - team.service.release.manager + - team.service.release.view + - team.team.member.view + - name: service_developer + cname: 服务开发者 + permits: + - team.service.upstream.manager + - team.service.upstream.view + - team.service.api.manager + - team.service.api.view + - team.service.release.manager + - team.service.release.view + - team.team.member.view + - name: application_admin + cname: 应用管理员 + permits: + - team.application.application.manager + - team.application.authorization.manager + - team.application.authorization.view + - team.application.subscription.manager + - team.application.subscription.view + - team.team.member.view + - name: application_developer + cname: 应用开发者 + permits: + - team.application.authorization.view + - team.application.subscription.manager + - team.application.subscription.view + - team.team.member.view + default: true \ No newline at end of file diff --git a/resources/permit/permit.go b/resources/permit/permit.go new file mode 100644 index 00000000..ceb76116 --- /dev/null +++ b/resources/permit/permit.go @@ -0,0 +1,34 @@ +package permit + +import ( + _ "embed" + + "github.com/eolinker/go-common/permit" + "gopkg.in/yaml.v3" +) + +var ( + + //go:embed permit.yml + data []byte +) + +func init() { + + //reset() + +} +func reset() { + pConfig := make(map[string]map[string][]string) + err := yaml.Unmarshal(data, &pConfig) + if err != nil { + panic(err) + } + for group, rules := range pConfig { + for access, paths := range rules { + av := permit.FormatAccess(group, access) + permit.AddPermitRule(av, paths...) + } + } + +} diff --git a/resources/permit/permit.yml b/resources/permit/permit.yml new file mode 100644 index 00000000..4b8bc1ea --- /dev/null +++ b/resources/permit/permit.yml @@ -0,0 +1,233 @@ +system: + monitor: + - "GET:/api/v1/monitor" + - "POST:/api/v1/monitor" + - "PUT:/api/v1/monitor" + - "DELETE:/api/v1/monitor" + service_categories_setting: + - "GET:/api/v1/categories" + - "POST:/api/v1/catalogue" + - "PUT:/api/v1/catalogue" + - "DELETE:/api/v1/catalogue" + team_manager: + - "GET:/api/v1/manager/teams" + - "GET:/api/v1/manager/team" + - "POST:/api/v1/manager/team" + - "PUT:/api/v1/manager/team" + - "DELETE:/api/v1/manager/team" + user_manager: + - "GET:/api/v1/user/departments" + - "GET:/api/v1/user/department" + - "POST:/api/v1/user/department" + - "PUT:/api/v1/user/department" + - "DELETE:/api/v1/user/department" + - "POST:/api/v1/user/department/member" + - "DELETE:/api/v1/user/department/member" + - "POST:/api/v1/user/department/member/remove" + - "GET:/api/v1/user/accounts" + - "POST:/api/v1/user/account" + - "POST:/api/v1/user/account/enable" + - "POST:/api/v1/user/account/disable" + - "DELETE:/api/v1/user/account" +# - "GET:/api/v1/simple/member" + + user_group: + - "GET:/api/v1/user/groups" + - "GET:/api/v1/user/group" + - "GET:/api/v1/user/group/:id" + - "POST:/api/v1/user/group" + - "PUT:/api/v1/user/group" + - "PUT:/api/v1/user/group/:id" + - "DELETE:/api/v1/user/group/:id" + - "DELETE:/api/v1/user/group" + - "POST:/api/v1/user/group/member" + - "DELETE:/api/v1/user/group/member" + - "GET:/api/v1/user/group/members" + organization_manager: + - "GET:/api/v1/manager/organizations" + - "GET:/api/v1/manager/organization" + - "GET:/api/v1/manager/organization/:id" + - "POST:/api/v1/manager/organization" + - "PUT:/api/v1/manager/organization" + - "PUT:/api/v1/manager/organization/:id" + - "DELETE:/api/v1/manager/organization/:id" + - "DELETE:/api/v1/manager/organization" + - "GET:/api/v1/manager/organization/partitions" + - "POST:/api/v1/simple/organizations" + + role_manager: + - "GET:/api/v1/manage/roles" + - "GET:/api/v1/manage/role" + - "GET:/api/v1/manage/role/:id" + - "POST:/api/v1/manage/role" + - "PUT:/api/v1/manage/role" + - "PUT:/api/v1/manage/role/:id" + - "DELETE:/api/v1/manage/role/:id" + - "DELETE:/api/v1/manage/role" +# - "POST:/api/v1/simple/roles" + system_permission_setting: + - "GET:/api/v1/system/permissions" + - "GET:/api/v1/system/permission/options" + - "GET:/api/v1/system/permission/options/team" + - "GET:/api/v1/system/permission/options/project" + - "POST:/api/v1/system/permission" + - "DELETE:/api/v1/system/permission" + - "POST:/api/v1/system/permission/team" + - "DELETE:/api/v1/system/permission/team" + - "POST:/api/v1/system/permission/project" + - "DELETE:/api/v1/system/permission/project" + devops: + + environs_setting: + - "GET:/api/v1/partitions" + - "POST:/api/v1/partition" + - "DELETE:/api/v1/partition" + - "PUT:/api/v1/partition" + - "GET:/api/v1/partition" + - "GET:/api/v1/partition/certificates" + - "POST:/api/v1/partition/certificate" + - "DELETE:/api/v1/partition/certificate" + - "PUT:/api/v1/partition/certificate" + - "GET:/api/v1/partition/certificate" + - "GET:/api/v1/partition/clusters" + - "GET:/api/v1/partition/cluster" + - "POST:/api/v1/partition/cluster" + - "DELETE:/api/v1/partition/cluster" + - "PUT:/api/v1/partition/cluster" + - "PUT:/api/v1/partition/cluster/reset" + - "GET:/api/v1/partition/cluster/nodes" +team: + project_manager: + - "GET:/api/v1/team/projects" + - "POST:/api/v1/team/project" + - "PUT:/api/v1/team/project" + - "GET:/api/v1/team/members/simple" + - "DELETE:/api/v1/team/project" + team_manager: + - "PUT:/api/v1/team" + - "DELETE:/api/v1/team" + - "POST:/api/v1/team/member" + - "DELETE:/api/v1/team/member" + - "GET:/api/v1/team/members" + + project_view: + - "GET:/api/v1/team/projects" + team_permission_setting: + - "GET:/api/v1/team/setting/permissions" + - "GET:/api/v1/team/setting/permission/options" + - "GET:/api/v1/team/setting/permission/options/project" + - "POST:/api/v1/team/setting/permission" + - "DELETE:/api/v1/team/setting/permission" + - "POST:/api/v1/team/setting/permission/project" + - "DELETE:/api/v1/team/setting/permission/project" + team_setting: + - "PUT:/api/v1/team" +# - "GET:/api/v1/team" + member_setting: + - "GET:/api/v1/team/members" + - "POST:/api/v1/team/member" + - "DELETE:/api/v1/team/member" + - "GET:/api/v1/team/members/toadd" + - "GET:/api/v1/team/members/simple" +project: + project_setting: +# - "GET:/api/v1/project/info" + - "PUT:/api/v1/project/info" + member_setting: + - "GET:/api/v1/project/members" + - "POST:/api/v1/project/member" + - "DELETE:/api/v1/project/member" + - "GET:/api/v1/project/members/toadd" + api_manager: + - "GET:/api/v1/project/apis" + - "GET:/api/v1/project/api/detail" + - "GET:/api/v1/project/api/detail/simple" + - "GET:/api/v1/project/api/doc" + - "GET:/api/v1/project/api/proxy" + - "GET:/api/v1/project/apis/simple" + - "POST:/api/v1/project/api" + - "PUT:/api/v1/project/api" + - "DELETE:/api/v1/project/api" + - "POST:/api/v1/project/api/copy" + - "GET:/api/v1/project/api/define" + api_view: + - "GET:/api/v1/project/apis" + - "GET:/api/v1/project/api/detail" + - "GET:/api/v1/project/api/detail/simple" + - "GET:/api/v1/project/api/doc" + - "GET:/api/v1/project/api/proxy" + - "GET:/api/v1/project/apis/simple" + upstream_manager: + - "GET:/api/v1/project/upstreams" + - "POST:/api/v1/project/upstream" + - "PUT:/api/v1/project/upstream" + - "DELETE:/api/v1/project/upstream" + - "GET:/api/v1/project/upstream" + - "DELETE:/api/v1/project/upstream/:id" + upstream_view: + - "GET:/api/vi/project/upstreams" + - "GET:/api/v1/project/upstream" + service_manager: + - "POST:/api/v1/project/service" + - "GET:/api/v1/project/services" + - "GET:/api/v1/project/service/doc" + - "GET:/api/v1/project/service/info" + - "PUT:/api/v1/project/service/enable" + - "PUT:/api/v1/project/service/disable" + - "PUT:/api/v1/project/service/doc" + - "PUT:/api/v1/project/service/info" + - "DELETE:/api/v1/project/service" + service_view: + - "GET:/api/v1/project/services" + - "GET:/api/v1/project/service/doc" + - "GET:/api/v1/project/service/info" + subscribe_view: + - "GET:/api/v1/project/subscriptions" + - "GET:/api/v1/project/subscription/approval" + subscribe_apply: + - "GET:/api/v1/project/subscriptions" + - "GET:/api/v1/project/subscription/approval" + - "POST:/api/v1/project/subscription/cancel" + - "POST:/api/v1/project/subscription/cancel/application" + subscribers_manager: + - "GET:/api/v1/project/subscribers" + - "POST:/api/v1/project/subscriber" + - "DELETE:/api/v1/project/subscriber" + subscribe_approval: + - "GET:/api/v1/project/approval/subscribers" + - "POST:/api/v1/project/approval/subscriber" + - "GET:/api/v1/project/approval/subscriber" + authentication_view: + - "GET:/api/v1/project/authorization" + - "GET:/api/v1/project/authorizations" + - "GET:/api/v1/project/authorization/drivers" + - "GET:/api/v1/project/authorization/details" + authentication_manager: + - "GET:/api/v1/project/authorization" + - "GET:/api/v1/project/authorizations" + - "GET:/api/v1/project/authorization/drivers" + - "GET:/api/v1/project/authorization/details" + - "PUT:/api/v1/project/authorization" + - "POST:/api/v1/project/authorization" + - "DELETE:/api/v1/project/authorization" + publish_manager: + - "GET:/api/v1/project/publish/Check" + - "DELETE:/api/v1/project/publish/close" + - "GET:/api/v1/project/publish" + - "GET:/api/v1/project/publishs" + - "POST:/api/v1/project/publish/release" + - "POST:/api/v1/project/publish/apply" + - "PUT:/api/v1/project/publish/execute" + - "DELETE:/api/v1/project/publish/stop" + + publish_approve: + - "POST:/api/v1/project/publish/do" + - "GET:/api/v1/project/publish" + - "GET:/api/v1/project/publishs" + - "PUT:/api/v1/project/publish/accept" + - "PUT:/api/v1/project/publish/refuse" + permission_manager: + - "GET:/api/v1/project/setting/permissions" + - "GET:/api/v1/project/setting/permission/options" + - "POST:/api/v1/project/setting/permission" + - "DELETE:/api/v1/project/setting/permission" \ No newline at end of file diff --git a/resources/permit/template.go b/resources/permit/template.go new file mode 100644 index 00000000..bd141a38 --- /dev/null +++ b/resources/permit/template.go @@ -0,0 +1,67 @@ +package permit + +import ( + _ "embed" + "fmt" + permit_type "github.com/APIParkLab/APIPark/service/permit-type" + "github.com/eolinker/eosc/log" + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/permit" + "github.com/eolinker/go-common/utils" + "gopkg.in/yaml.v3" + "reflect" +) + +var ( + //go:embed template.yml + templateData []byte +) +var ( + _ permit.IPermitInitialize = (*imlPermitData)(nil) +) + +type imlPermitData struct { + data map[string]map[string][]string +} + +func (i *imlPermitData) Grants() map[string]map[string][]string { + return i.data +} + +func init() { + autowire.Auto[permit.IPermitInitialize](func() reflect.Value { + v := new(imlPermitData) + permitData := make(map[string]map[string][]string) + err := yaml.Unmarshal(templateData, permitData) + if err != nil { + log.Fatal("read permit initialize data :", err) + } + v.data = make(map[string]map[string][]string) + for group, grants := range permitData { + domain, has := domainForGroup[group] + if !has { + continue + } + + if _, h := v.data[domain]; !h { + v.data[domain] = make(map[string][]string) + } + + for access, ts := range grants { + v.data[domain][fmt.Sprintf("%s.%s", group, access)] = utils.SliceToSlice(ts, func(s string) string { + return permit_type.Special.KeyOf(s) + }) + } + } + + return reflect.ValueOf(v) + }) +} + +var ( + domainForGroup = map[string]string{ + "system": "/", + "team": "/template/team", + "project": "/template/project", + } +) diff --git a/resources/permit/template.yml b/resources/permit/template.yml new file mode 100644 index 00000000..9668bfe7 --- /dev/null +++ b/resources/permit/template.yml @@ -0,0 +1,94 @@ +system: +team: + project_manager: + - "team_master" + - "team_member" + + project_view: + - "team_member" + - "team_master" + + team_permission_setting: + - "team_master" + + team_setting: + - "team_master" + + member_setting: + - "team_master" + +project: + project_setting: + - "team_master" + - "project_master" + member_setting: + - "team_master" + - "project_master" + + api_manager: + - "team_master" + - "project_master" + - "project_member" + api_view: + - "team_master" + - "project_master" + - "project_member" + + upstream_manager: + - "team_master" + - "project_master" + - "project_member" + + upstream_view: + - "team_master" + - "project_master" + - "project_member" + + service_manager: + - "team_master" + - "project_master" + + service_view: + - "team_master" + - "project_master" + + subscribe_view: + - "team_master" + - "project_master" + - "project_member" + + subscribe_apply: + - "team_master" + - "project_master" + - "project_member" + + subscribers_manager: + - "team_master" + - "project_master" + - "project_member" + + subscribe_approval: + - "team_master" + - "project_master" + + authentication_view: + - "team_master" + - "project_master" + - "project_member" + + authentication_manager: + - "team_master" + - "project_master" + + publish_manager: + - "team_master" + - "project_master" + - "project_member" + + publish_approve: + - "team_master" + - "project_master" + + permission_manager: + - "team_master" + - "project_master" \ No newline at end of file diff --git a/resources/plugin/define.go b/resources/plugin/define.go new file mode 100644 index 00000000..a793be8f --- /dev/null +++ b/resources/plugin/define.go @@ -0,0 +1,17 @@ +package plugin + +import "github.com/APIParkLab/APIPark/model/plugin_model" + +type defineT struct { + Id string `json:"id,omitempty" yaml:"id"` + Kind string `json:"kind,omitempty" yaml:"kind"` + CName string `json:"CName,omitempty" yaml:"CName"` + Status string `json:"status,omitempty" yaml:"status"` + Config plugin_model.ConfigType `json:"config" yaml:"config"` + Desc string `json:"desc,omitempty" yaml:"desc"` + //Render plugin_model.Render `json:"render,omitempty" yaml:"render"` +} + +type defineRender struct { + Render plugin_model.Render `json:"render,omitempty" yaml:"render"` +} diff --git a/resources/plugin/grpc_to_http.json b/resources/plugin/grpc_to_http.json new file mode 100644 index 00000000..2bbf0f4d --- /dev/null +++ b/resources/plugin/grpc_to_http.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"grpc_to_http","version":"innert","render":{"type":"object","eo:type":"object","properties":{"headers":{"type":"object","eo:type":"map","additionalProperties":{"type":"string","eo:type":"string"},"label":"额外头部"},"method":{"type":"string","eo:type":"string","enum":["POST","PUT","PATCH"],"label":"请求方式"},"path":{"type":"string","eo:type":"string","label":"请求路径"},"protobuf_id":{"type":"string","eo:type":"require","skill":"github.com/eolinker/apinto/grpc-transcode.transcode.IDescriptor","label":"Protobuf ID"},"query":{"type":"object","eo:type":"map","additionalProperties":{"type":"string","eo:type":"string"},"label":"query参数"}},"ui:sort":["path","method","protobuf_id","headers","query"],"required":["protobuf_id"]}} \ No newline at end of file diff --git a/resources/plugin/plugin-load.go b/resources/plugin/plugin-load.go new file mode 100644 index 00000000..a803cb8a --- /dev/null +++ b/resources/plugin/plugin-load.go @@ -0,0 +1,172 @@ +package plugin + +import ( + "context" + "embed" + _ "embed" + "encoding/json" + "fmt" + "gopkg.in/yaml.v3" + "log" + "strings" + + "github.com/APIParkLab/APIPark/model/plugin_model" + pluginModule "github.com/APIParkLab/APIPark/module/plugin-cluster" + "github.com/APIParkLab/APIPark/service/setting" + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/store" + "github.com/eolinker/go-common/utils" +) + +const ( + pluginVersionName = "system.plugin.version" +) + +var ( + //go:embed plugin.yml + embeddedPlugin []byte + //go:embed render/*.json + renders embed.FS +) + +type pluginLoad struct { + transaction store.ITransaction `autowired:""` + module pluginModule.IPluginClusterModule `autowired:""` + settingService setting.ISettingService `autowired:""` + version string + defines []*plugin_model.Define +} + +func (p *pluginLoad) OnInit() { + + p.transaction.Transaction(context.Background(), func(ctx context.Context) error { + value, has := p.settingService.Get(ctx, pluginVersionName) + if has { + if value >= p.version { + return nil + } + } + err := p.module.UpdateDefine(ctx, p.defines) + if err != nil { + return err + } + return p.settingService.Set(ctx, pluginVersionName, p.version, "system") + }) + + p.defines = nil + p.version = "" +} + +func (p *pluginLoad) UnmarshalYAML(value *yaml.Node) error { + + sorts := make([]string, 0) + var itemNodes []*yaml.Node + + for i := 0; i < len(value.Content); i += 2 { + v := value.Content[i] + switch strings.ToLower(v.Value) { + case "version": + p.version = value.Content[i+1].Value + continue + case "sort": + err := value.Content[i+1].Decode(&sorts) + if err != nil { + return err + } + continue + case "plugin": + itemNodes = value.Content[i+1].Content + + } + + } + items, err := pluginItemUnmarshalYAML(sorts, itemNodes) + if err != nil { + return err + } + p.defines = items + return nil +} +func pluginItemUnmarshalYAML(sorts []string, nodes []*yaml.Node) ([]*plugin_model.Define, error) { + if len(nodes) == 0 { + return nil, fmt.Errorf("yaml: error decoding plugin is empty") + } + type defineItem struct { + *plugin_model.Define + sort int + } + sortBase := len(sorts) + 1 + Items := make([]*defineItem, 0, len(nodes)/2-2) + for i := 0; i < len(nodes); i += 2 { + k := nodes[i].Value + v := nodes[i+1] + it := new(defineT) + + err := v.Decode(&it) + if err != nil { + log.Printf("yaml: error decoding %s: %v", v.Value, err) + return nil, err + } + Items = append(Items, &defineItem{ + + Define: &plugin_model.Define{ + Extend: it.Id, + Name: k, + Cname: it.CName, + Desc: it.Desc, + Kind: plugin_model.ParseKind(it.Kind), + Status: plugin_model.ParseStatus(it.Status), + Config: it.Config, + }, + sort: i + sortBase, + }) + } + + sortValues := make(map[string]int, len(sorts)) + for i, v := range sorts { + sortValues[v] = i + } + + for _, v := range Items { + if s, has := sortValues[v.Name]; has { + v.sort = s + } + extend := v.Extend + extend = strings.Replace(extend, ".", "_", -1) + //extend = strings.Replace(extend, "-", "_", -1) + extend = strings.Replace(extend, ":", "_", -1) + renderBody, err := renders.ReadFile(fmt.Sprintf("render/%s.json", extend)) + if err != nil { + return nil, err + } + renderObj := new(defineRender) + err = json.Unmarshal(renderBody, renderObj) + if err != nil { + return nil, err + } + v.Render = renderObj.Render + } + + utils.Sort(Items, func(i, j *defineItem) bool { + + return i.sort < j.sort + }) + for i, v := range Items { + v.sort = i + } + return utils.SliceToSlice(Items, func(v *defineItem) *plugin_model.Define { + return v.Define + }), nil +} +func init() { + loader := new(pluginLoad) + + err := yaml.Unmarshal(embeddedPlugin, loader) + // 释放内存 + embeddedPlugin = nil + if err != nil { + panic(fmt.Errorf("unmarshal plugin inner:%w", err)) + return + } + autowire.Autowired(loader) +} diff --git a/resources/plugin/plugin.yml b/resources/plugin/plugin.yml new file mode 100644 index 00000000..70a07ffc --- /dev/null +++ b/resources/plugin/plugin.yml @@ -0,0 +1,77 @@ +version: v5 +sort: + - "access_log" + - "monitor" + - "proxy_rewrite" + - "app" + - "access_relational" +plugin: + access_log: + id: eolinker.com:apinto:access_log + name: access_log + status: global + + monitor: + id: eolinker.com:apinto:monitor + name: monitor + status: global + + proxy_rewrite: + id: eolinker.com:apinto:proxy_rewrite_v2 + name: proxy_rewrite + status: enable + + extra_params: + id: eolinker.com:apinto:extra_params + name: extra_params + status: enable + app: + id: eolinker.com:apinto:plugin_app + name: app + status: global + config: + force_auth: true + access_relational: + id: eolinker.com:apinto:access_relational + name: access_relational + status: global + config: + rules: + - a: "service_of_api:#{api}" + b: "subscription_service:#{application}" + response: + status_code: 403 + content_typ: "text/plan" + charset: "utf-8" + body: "Forbidden" + + strategy_visit: + id: eolinker.com:apinto:strategy-plugin-visit + name: strategy_visit + status: global + + strategy_grey: + id: eolinker.com:apinto:strategy-plugin-grey + name: strategy_grey + status: global + + strategy_limiting: + id: eolinker.com:apinto:strategy-plugin-limiting + name: strategy_limiting + status: global + config: + cache: redis@output + + strategy_fuse: + id: eolinker.com:apinto:strategy-plugin-fuse + name: strategy_fuse + status: global + config: + cache: redis@output + + strategy_cache: + id: eolinker.com:apinto:strategy-plugin-cache + name: strategy_cache + status: global + config: + cache: redis@output \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_access_log.json b/resources/plugin/render/eolinker_com_apinto_access_log.json new file mode 100644 index 00000000..ed723ca1 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_access_log.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"access_log","version":"innert","render":{"type":"object","eo:type":"object","properties":{"output":{"type":"array","eo:type":"array","items":{"type":"string","eo:type":"require"},"skill":"github.com/eolinker/apinto/http-entry.http-entry.IOutput","label":"输出器列表"}},"ui:sort":["output"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_access_relational.json b/resources/plugin/render/eolinker_com_apinto_access_relational.json new file mode 100644 index 00000000..c5282903 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_access_relational.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"access_relational","version":"innert","render":{"type":"object","eo:type":"object","properties":{"response":{"type":"object","eo:type":"object","description":"请求被拦截时响应的内容","properties":{"body":{"type":"string","eo:type":"string","description":"body模版, 支持 #{label} 语法","label":"Body"},"charset":{"type":"string","eo:type":"string","label":"Charset"},"content_type":{"type":"string","eo:type":"string","label":"Content-Type"},"headers":{"type":"array","eo:type":"array","items":{"type":"object","eo:type":"object","properties":{"key":{"type":"string","eo:type":"string","description":"header 的key,支持 #{label}","label":"header key"},"value":{"type":"string","eo:type":"string","description":"header 的值, 支持#{label}","label":"header value"}},"ui:sort":["key","value"]},"label":"Header参数"},"status_code":{"type":"integer","eo:type":"integer","format":"int32","label":"HTTP状态码"}},"ui:sort":["status_code","content_type","charset","headers","body"],"label":"响应内容"},"rules":{"type":"array","eo:type":"array","description":"规则列表, 规则为空时,不执行拦截, 多个规则时,有任意规则通过则均放行, ","items":{"type":"object","eo:type":"object","properties":{"a":{"type":"string","eo:type":"string","description":"A key 规则,支持#{}的metrics语法","label":"Key A"},"b":{"type":"string","eo:type":"string","description":"B Key 规则,支持#{}的metrics语法","label":"key B"}},"ui:sort":["a","b"]},"label":"规则"}},"ui:sort":["rules","response"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_acl.json b/resources/plugin/render/eolinker_com_apinto_acl.json new file mode 100644 index 00000000..fa9030ca --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_acl.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"acl","version":"innert","render":{"type":"object","eo:type":"object","properties":{"allow":{"type":"array","eo:type":"array","items":{"type":"string","eo:type":"string"}},"deny":{"type":"array","eo:type":"array","items":{"type":"string","eo:type":"string"}},"hide_groups_header":{"type":"boolean","eo:type":"boolean"}},"ui:sort":["allow","deny","hide_groups_header"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_body_check.json b/resources/plugin/render/eolinker_com_apinto_body_check.json new file mode 100644 index 00000000..1d6340d1 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_body_check.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"body_check","version":"innert","render":{"type":"object","eo:type":"object","properties":{"allowed_payload_size":{"type":"integer","eo:type":"integer","format":"int32","label":"允许的最大请求体大小"},"is_empty":{"type":"boolean","eo:type":"boolean","label":"是否允许为空"}},"ui:sort":["is_empty","allowed_payload_size"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_circuit_breaker.json b/resources/plugin/render/eolinker_com_apinto_circuit_breaker.json new file mode 100644 index 00000000..59448653 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_circuit_breaker.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"circuit_breaker","version":"innert","render":{"type":"object","eo:type":"object","properties":{"body":{"type":"string","eo:type":"string","label":"熔断状态下的返回响应体"},"break_period":{"type":"integer","eo:type":"integer","description":"最小值:1","format":"int64","minimum":1,"label":"熔断期"},"breaker_code":{"type":"integer","eo:type":"integer","description":"最小值:100","format":"int32","minimum":100,"label":"熔断状态下返回的响应状态码"},"failure_percent":{"type":"number","eo:type":"number","description":"最小值:0,最大值:1","format":"double","minimum":0,"maximum":1,"label":"监控期内的请求错误率"},"headers":{"type":"object","eo:type":"map","additionalProperties":{"type":"string","eo:type":"string"},"label":"熔断状态下新增的返回头部值"},"match_codes":{"type":"string","eo:type":"string","description":"多个状态码之间使用英文逗号隔开","label":"匹配状态码"},"minimum_requests":{"type":"integer","eo:type":"integer","description":"最小值:1","format":"int32","minimum":1,"label":"最低熔断阀值,达到熔断状态的最少请求次数"},"monitor_period":{"type":"integer","eo:type":"integer","description":"单位:秒,最小值:1","format":"int32","minimum":1,"label":"监控期"},"success_counts":{"type":"integer","eo:type":"integer","description":"最小值:1","format":"int32","minimum":1,"label":"连续请求成功次数,半开放状态下请求成功次数达到后会转变成健康状态"}},"ui:sort":["match_codes","monitor_period","minimum_requests","failure_percent","break_period","success_counts","breaker_code","headers","body"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_cors.json b/resources/plugin/render/eolinker_com_apinto_cors.json new file mode 100644 index 00000000..0765deec --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_cors.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"cors","version":"innert","render":{"type":"object","eo:type":"object","properties":{"allow_credentials":{"type":"boolean","eo:type":"boolean","label":"请求中是否携带cookie"},"allow_headers":{"type":"string","eo:type":"string","description":"多种请求方式用英文逗号隔开","default":"*","label":"允许跨域访问时请求方携带的非CORS规范以外的Header"},"allow_methods":{"type":"string","eo:type":"string","description":"多种请求方式用英文逗号隔开","default":"*","label":"允许通过的请求方式"},"allow_origins":{"type":"string","eo:type":"string","default":"*","label":"允许跨域访问的Origin"},"expose_headers":{"type":"string","eo:type":"string","description":"多种请求方式用英文逗号隔开","default":"*","label":"允许跨域访问时响应方携带的非CORS规范以外的Header"},"max_age":{"type":"integer","eo:type":"integer","description":"浏览器缓存CORS结果的最大时间","format":"int32","default":5,"minimum":1}},"ui:sort":["allow_origins","allow_methods","allow_credentials","allow_headers","expose_headers","max_age"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_counter.json b/resources/plugin/render/eolinker_com_apinto_counter.json new file mode 100644 index 00000000..c1a76db6 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_counter.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"counter","version":"innert","render":{"type":"object","eo:type":"object","properties":{"cache":{"type":"string","eo:type":"require","skill":"github.com/eolinker/apinto/resources.resources.ICache","label":"缓存计数器"},"count":{"type":"object","eo:type":"object","properties":{"key":{"type":"string","eo:type":"string","label":"参数名称(支持json path)"},"max":{"type":"integer","eo:type":"integer","format":"int64","label":"计数最大值"},"request_body_type":{"type":"string","eo:type":"string","enum":["form-data","json","multipart-formdata"],"label":"请求体类型"},"separator":{"type":"string","eo:type":"string","switch":"separator_type===splite","label":"分隔符"},"separator_type":{"type":"string","eo:type":"string","enum":["splite","array","length"],"label":"分割类型"}},"ui:sort":["request_body_type","key","separator","separator_type","max"],"label":"计数规则"},"key":{"type":"string","eo:type":"string","label":"格式化Key"},"match":{"type":"object","eo:type":"object","properties":{"params":{"type":"array","eo:type":"array","items":{"type":"object","eo:type":"object","properties":{"key":{"type":"string","eo:type":"string"},"kind":{"type":"string","eo:type":"string","enum":["int","string","bool"],"default":"string"},"value":{"type":"array","eo:type":"array","items":{"type":"string","eo:type":"string"}}},"ui:sort":["key","kind","value"]},"label":"匹配参数列表"},"status_codes":{"type":"array","eo:type":"array","items":{"type":"integer","eo:type":"integer","format":"int32"},"label":"匹配响应状态码列表"},"type":{"type":"string","eo:type":"string","enum":["json"],"label":"匹配类型"}},"ui:sort":["params","status_codes","type"],"label":"响应匹配规则"}},"ui:sort":["key","cache","match","count"],"required":["key"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_data_transform.json b/resources/plugin/render/eolinker_com_apinto_data_transform.json new file mode 100644 index 00000000..a902c35a --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_data_transform.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"data_transform","version":"innert","render":{"type":"object","eo:type":"object","properties":{"error_type":{"type":"string","eo:type":"string","enum":["json","xml"],"default":"json","label":"报错数据类型"},"request_transform":{"type":"boolean","eo:type":"boolean","label":"请求转换"},"response_transform":{"type":"boolean","eo:type":"boolean","label":"响应转换"},"xml_declaration":{"type":"object","eo:type":"map","additionalProperties":{"type":"string","eo:type":"string"},"label":"XML声明"},"xml_root_tag":{"type":"string","eo:type":"string","label":"XML根标签"}},"ui:sort":["request_transform","response_transform","xml_root_tag","xml_declaration","error_type"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_dubbo2-proxy-rewrite.json b/resources/plugin/render/eolinker_com_apinto_dubbo2-proxy-rewrite.json new file mode 100644 index 00000000..8887f178 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_dubbo2-proxy-rewrite.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"dubbo2-proxy-rewrite","version":"innert","render":{"type":"object","eo:type":"object","properties":{"headers":{"type":"object","eo:type":"map","additionalProperties":{"type":"string","eo:type":"string"},"label":"headers"},"method":{"type":"string","eo:type":"string","label":"方法名称"},"service":{"type":"string","eo:type":"string","label":"服务名称"}},"ui:sort":["service","method","headers"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_dubbo2_to_http.json b/resources/plugin/render/eolinker_com_apinto_dubbo2_to_http.json new file mode 100644 index 00000000..f1f0fd0b --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_dubbo2_to_http.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"dubbo2_to_http","version":"innert","render":{"type":"object","eo:type":"object","properties":{"content_type":{"type":"string","eo:type":"string","enum":["application/json"],"label":"ContentType"},"method":{"type":"string","eo:type":"string","enum":["POST","GET","HEAD","PUT","PATCH","DELETE","CONNECT","OPTIONS","TRACE"],"label":"方法"},"params":{"type":"array","eo:type":"array","items":{"type":"object","eo:type":"object","properties":{"class_name":{"type":"string","eo:type":"string","label":"class_name"},"field_name":{"type":"string","eo:type":"string","label":"字段名"}},"ui:sort":["class_name","field_name"],"required":["class_name"]},"label":"参数解析"},"path":{"type":"string","eo:type":"string","label":"转发路径"}},"ui:sort":["method","path","content_type","params"],"required":["method","path","content_type","params"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_extra_params.json b/resources/plugin/render/eolinker_com_apinto_extra_params.json new file mode 100644 index 00000000..9c426c08 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_extra_params.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"extra_params","version":"innert","render":{"type":"object","eo:type":"object","properties":{"error_type":{"type":"string","eo:type":"string","enum":["text","json"],"label":"报错输出格式"},"params":{"type":"array","eo:type":"array","items":{"type":"object","eo:type":"object","properties":{"conflict":{"type":"string","eo:type":"string","enum":["origin","convert","error"],"label":"参数冲突时的处理方式"},"name":{"type":"string","eo:type":"string","label":"参数名"},"position":{"type":"string","eo:type":"string","enum":["header","query","body"],"label":"参数位置"},"value":{"type":"string","eo:type":"string","label":"参数值"}},"ui:sort":["name","position","value","conflict"]},"label":"参数列表"}},"ui:sort":["params","error_type"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_extra_params_v2.json b/resources/plugin/render/eolinker_com_apinto_extra_params_v2.json new file mode 100644 index 00000000..ffb9380a --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_extra_params_v2.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"extra_params_v2","version":"innert","render":{"type":"object","eo:type":"object","properties":{"error_type":{"type":"string","eo:type":"string","enum":["text","json"],"label":"报错输出格式"},"params":{"type":"array","eo:type":"array","items":{"type":"object","eo:type":"object","properties":{"conflict":{"type":"string","eo:type":"string","enum":["origin","convert","error"],"label":"参数冲突时的处理方式"},"name":{"type":"string","eo:type":"string","label":"参数名"},"position":{"type":"string","eo:type":"string","enum":["header","query","body"],"label":"参数位置"},"type":{"type":"string","eo:type":"string","enum":["string","int","float","bool","$datetime","$md5","$timestamp","$concat","$hmac-sha256"],"label":"参数类型"},"value":{"type":"array","eo:type":"array","items":{"type":"string","eo:type":"string"},"label":"参数值列表"}},"ui:sort":["name","type","position","value","conflict"]},"label":"参数列表"},"request_body_type":{"type":"string","eo:type":"string","enum":["form-data","json","multipart-formdata"],"label":"请求体类型"}},"ui:sort":["params","request_body_type","error_type"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_grpc-proxy_write.json b/resources/plugin/render/eolinker_com_apinto_grpc-proxy_write.json new file mode 100644 index 00000000..ada96b99 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_grpc-proxy_write.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"grpc-proxy_write","version":"innert","render":{"type":"object","eo:type":"object","properties":{"authority":{"type":"string","eo:type":"string","label":"虚拟主机域名(Authority)"},"headers":{"type":"object","eo:type":"map","additionalProperties":{"type":"string","eo:type":"string"},"label":"请求头部"},"method":{"type":"string","eo:type":"string","label":"方法名称"},"service":{"type":"string","eo:type":"string","label":"服务名称"}},"ui:sort":["service","method","authority","headers"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_grpc_to_http.json b/resources/plugin/render/eolinker_com_apinto_grpc_to_http.json new file mode 100644 index 00000000..2bbf0f4d --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_grpc_to_http.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"grpc_to_http","version":"innert","render":{"type":"object","eo:type":"object","properties":{"headers":{"type":"object","eo:type":"map","additionalProperties":{"type":"string","eo:type":"string"},"label":"额外头部"},"method":{"type":"string","eo:type":"string","enum":["POST","PUT","PATCH"],"label":"请求方式"},"path":{"type":"string","eo:type":"string","label":"请求路径"},"protobuf_id":{"type":"string","eo:type":"require","skill":"github.com/eolinker/apinto/grpc-transcode.transcode.IDescriptor","label":"Protobuf ID"},"query":{"type":"object","eo:type":"map","additionalProperties":{"type":"string","eo:type":"string"},"label":"query参数"}},"ui:sort":["path","method","protobuf_id","headers","query"],"required":["protobuf_id"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_gzip.json b/resources/plugin/render/eolinker_com_apinto_gzip.json new file mode 100644 index 00000000..0fc8eb15 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_gzip.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"gzip","version":"innert","render":{"type":"object","eo:type":"object","properties":{"min_length":{"type":"integer","eo:type":"integer","description":"待压缩内容的最小长度","format":"int32","label":"长度"},"types":{"type":"array","eo:type":"array","description":"需要压缩的响应content-type类型列表","items":{"type":"string","eo:type":"string"},"label":"content-type列表"},"vary":{"type":"boolean","eo:type":"boolean","label":"是否加上Vary头部"}},"ui:sort":["types","min_length","vary"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_http-mocking.json b/resources/plugin/render/eolinker_com_apinto_http-mocking.json new file mode 100644 index 00000000..f85e0875 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_http-mocking.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"http-mocking","version":"innert","render":{"type":"object","eo:type":"object","properties":{"content_type":{"type":"string","eo:type":"string","enum":["application/json"],"label":"响应 Content-Type"},"response_example":{"type":"string","eo:type":"string","description":"与Json Schema字段二选一","format":"text","label":"响应Body"},"response_header":{"type":"object","eo:type":"map","additionalProperties":{"type":"string","eo:type":"string"},"label":"响应头"},"response_schema":{"type":"string","eo:type":"string","description":"Mock生成的Json Schema语法数据,与响应Body字段二选一","format":"text","label":"Json Schema"},"response_status":{"type":"integer","eo:type":"integer","description":"仅http路由有效","format":"int32","default":200,"label":"响应状态码"}},"ui:sort":["response_status","content_type","response_example","response_schema","response_header"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_http_to_dubbo2.json b/resources/plugin/render/eolinker_com_apinto_http_to_dubbo2.json new file mode 100644 index 00000000..2ee39f69 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_http_to_dubbo2.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"http_to_dubbo2","version":"innert","render":{"type":"object","eo:type":"object","properties":{"method":{"type":"string","eo:type":"string","label":"方法名称"},"params":{"type":"array","eo:type":"array","items":{"type":"object","eo:type":"object","properties":{"class_name":{"type":"string","eo:type":"string","label":"class_name"},"field_name":{"type":"string","eo:type":"string","label":"根字段名"}},"ui:sort":["class_name","field_name"],"required":["class_name"]},"label":"参数"},"service":{"type":"string","eo:type":"string","label":"服务名称"}},"ui:sort":["service","method","params"],"required":["service","method","params"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_http_to_grpc.json b/resources/plugin/render/eolinker_com_apinto_http_to_grpc.json new file mode 100644 index 00000000..e44c946c --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_http_to_grpc.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"http_to_grpc","version":"innert","render":{"type":"object","eo:type":"object","properties":{"authority":{"type":"string","eo:type":"string","label":"虚拟主机域名(Authority)"},"format":{"type":"string","eo:type":"string","enum":["json"],"label":"数据格式"},"headers":{"type":"object","eo:type":"map","additionalProperties":{"type":"string","eo:type":"string"},"label":"额外头部"},"method":{"type":"string","eo:type":"string","label":"方法名称"},"protobuf_id":{"type":"string","eo:type":"require","skill":"github.com/eolinker/apinto/grpc-transcode.transcode.IDescriptor","switch":"reflect === false","label":"Protobuf ID"},"reflect":{"type":"boolean","eo:type":"boolean","label":"反射"},"service":{"type":"string","eo:type":"string","label":"服务名称"}},"ui:sort":["service","method","authority","format","reflect","protobuf_id","headers"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_ip_restriction.json b/resources/plugin/render/eolinker_com_apinto_ip_restriction.json new file mode 100644 index 00000000..28f187fa --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_ip_restriction.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"ip_restriction","version":"innert","render":{"type":"object","eo:type":"object","properties":{"ip_black_list":{"type":"array","eo:type":"array","items":{"type":"string","eo:type":"string"},"label":"ip黑名单列表"},"ip_list_type":{"type":"string","eo:type":"string","enum":["white","black"],"label":"列表类型"},"ip_white_list":{"type":"array","eo:type":"array","items":{"type":"string","eo:type":"string"},"label":"ip白名单列表"}},"ui:sort":["ip_list_type","ip_white_list","ip_black_list"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_js_inject.json b/resources/plugin/render/eolinker_com_apinto_js_inject.json new file mode 100644 index 00000000..e7f14e85 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_js_inject.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"js_inject","version":"innert","render":{"type":"object","eo:type":"object","properties":{"inject":{"type":"string","eo:type":"string","label":"注入代码"},"match_content_type":{"type":"array","eo:type":"array","items":{"type":"string","eo:type":"string"},"label":"匹配的Content-Type"},"variables":{"type":"array","eo:type":"array","items":{"type":"object","eo:type":"object","properties":{"key":{"type":"string","eo:type":"string","label":"变量名"},"value":{"type":"string","eo:type":"string","label":"变量值"}},"ui:sort":["key","value"]},"label":"变量列表"}},"ui:sort":["variables","inject","match_content_type"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_monitor.json b/resources/plugin/render/eolinker_com_apinto_monitor.json new file mode 100644 index 00000000..cf7d2482 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_monitor.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"monitor","version":"innert","render":{"type":"object","eo:type":"object","properties":{"output":{"type":"array","eo:type":"array","items":{"type":"string","eo:type":"require"},"skill":"github.com/eolinker/apinto/monitor-entry.monitor-entry.IOutput","label":"输出器列表"}},"ui:sort":["output"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_oauth2.json b/resources/plugin/render/eolinker_com_apinto_oauth2.json new file mode 100644 index 00000000..27164d25 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_oauth2.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"oauth2","version":"innert","render":{"type":"object","eo:type":"object","properties":{"accept_http_if_already_terminated":{"type":"boolean","eo:type":"boolean"},"enable_authorization_code":{"type":"boolean","eo:type":"boolean"},"enable_client_credentials":{"type":"boolean","eo:type":"boolean"},"enable_implicit_grant":{"type":"boolean","eo:type":"boolean"},"mandatory_scope":{"type":"boolean","eo:type":"boolean"},"persistent_refresh_token":{"type":"boolean","eo:type":"boolean"},"provision_key":{"type":"string","eo:type":"string"},"refresh_token_ttl":{"type":"integer","eo:type":"integer","format":"int32"},"reuse_refresh_token":{"type":"boolean","eo:type":"boolean"},"scopes":{"type":"array","eo:type":"array","items":{"type":"string","eo:type":"string"}},"token_expiration":{"type":"integer","eo:type":"integer","format":"int32"}},"ui:sort":["accept_http_if_already_terminated","enable_authorization_code","enable_client_credentials","enable_implicit_grant","mandatory_scope","persistent_refresh_token","provision_key","refresh_token_ttl","reuse_refresh_token","scopes","token_expiration"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_params_check.json b/resources/plugin/render/eolinker_com_apinto_params_check.json new file mode 100644 index 00000000..043fed43 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_params_check.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"params_check","version":"innert","render":{"type":"object","eo:type":"object","properties":{"params":{"type":"array","eo:type":"array","items":{"type":"object","eo:type":"object","properties":{"match_text":{"type":"string","eo:type":"string","label":"匹配文本"},"name":{"type":"string","eo:type":"string","label":"参数名"},"position":{"type":"string","eo:type":"string","enum":["query","header","body"],"label":"参数位置"}},"ui:sort":["name","position","match_text"]},"label":"参数列表"}},"ui:sort":["params"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_params_transformer.json b/resources/plugin/render/eolinker_com_apinto_params_transformer.json new file mode 100644 index 00000000..44156f4c --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_params_transformer.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"params_transformer","version":"innert","render":{"type":"object","eo:type":"object","properties":{"error_type":{"type":"string","eo:type":"string","enum":["text","json"],"label":"报错输出格式"},"params":{"type":"array","eo:type":"array","items":{"type":"object","eo:type":"object","properties":{"name":{"type":"string","eo:type":"string","label":"待映射参数名称"},"position":{"type":"string","eo:type":"string","enum":["header","query","body"],"label":"待映射参数所在位置"},"proxy_name":{"type":"string","eo:type":"string","label":"目标参数名称"},"proxy_position":{"type":"string","eo:type":"string","enum":["header","query","body"],"label":"目标参数所在位置"},"required":{"type":"boolean","eo:type":"boolean","label":"待映射参数是否必含"}},"ui:sort":["name","position","proxy_name","proxy_position","required"]},"label":"参数列表"},"remove":{"type":"boolean","eo:type":"boolean","label":"映射后删除原参数"}},"ui:sort":["params","remove","error_type"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_plugin_app.json b/resources/plugin/render/eolinker_com_apinto_plugin_app.json new file mode 100644 index 00000000..a9b2d2a8 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_plugin_app.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"plugin_app","version":"innert","render":{"type":"object","eo:type":"object"}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_prometheus.json b/resources/plugin/render/eolinker_com_apinto_prometheus.json new file mode 100644 index 00000000..619872ea --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_prometheus.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"prometheus","version":"innert","render":{"type":"object","eo:type":"object","properties":{"metrics":{"type":"array","eo:type":"array","items":{"type":"string","eo:type":"string"},"label":"指标名列表"},"output":{"type":"array","eo:type":"array","items":{"type":"string","eo:type":"require"},"skill":"github.com/eolinker/apinto/metric-entry.metric-entry.IMetrics","label":"prometheus Output列表"}},"ui:sort":["output","metrics"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_proxy_mirror.json b/resources/plugin/render/eolinker_com_apinto_proxy_mirror.json new file mode 100644 index 00000000..fed649de --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_proxy_mirror.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"proxy_mirror","version":"innert","render":{"type":"object","eo:type":"object","properties":{"addr":{"type":"string","eo:type":"string","description":"镜像服务地址, 需要包含scheme","label":"服务地址"},"host":{"type":"string","eo:type":"string","description":"指定上游请求的host,只有在 转发域名 配置为 rewrite 时有效","switch":"pass_host==='rewrite'","label":"新host"},"pass_host":{"type":"string","eo:type":"string","description":"请求发给上游时的 host 设置选型,pass:将客户端的 host 透传给上游,node:使用addr中配置的host,rewrite:使用下面指定的host值","enum":["pass","node","rewrite"],"default":"pass","label":"转发域名"},"sample_conf":{"type":"object","eo:type":"object","properties":{"random_pivot":{"type":"integer","eo:type":"integer","format":"int32","label":"随机数锚点"},"random_range":{"type":"integer","eo:type":"integer","format":"int32","label":"随机数范围"}},"ui:sort":["random_range","random_pivot"],"label":"采样配置"},"timeout":{"type":"integer","eo:type":"integer","format":"int32","label":"请求超时时间"}},"ui:sort":["addr","sample_conf","timeout","pass_host","host"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_proxy_rewrite.json b/resources/plugin/render/eolinker_com_apinto_proxy_rewrite.json new file mode 100644 index 00000000..0eee69ae --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_proxy_rewrite.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"proxy_rewrite","version":"innert","render":{"type":"object","eo:type":"object","properties":{"headers":{"type":"object","eo:type":"map","description":"可对转发请求的头部进行新增,修改,删除。配置的kv对,不存在则新增,已存在则进行覆盖重写,但需要注意特殊头部字段只能在后面添加新值而不能覆盖。value为空字符串表示删除。","additionalProperties":{"type":"string","eo:type":"string"},"label":"请求头部"},"host":{"type":"string","eo:type":"string","label":"Host"},"regex_uri":{"type":"array","eo:type":"array","description":"该数组需要配置两个正则,第一个是匹配正则,第二个是替换正则。","items":{"type":"string","eo:type":"string"},"label":"正则替换路径(regex_uri)"},"uri":{"type":"string","eo:type":"string","label":"路径"}},"ui:sort":["uri","regex_uri","host","headers"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_proxy_rewrite_v2.json b/resources/plugin/render/eolinker_com_apinto_proxy_rewrite_v2.json new file mode 100644 index 00000000..f1fc6643 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_proxy_rewrite_v2.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"proxy_rewrite_v2","version":"innert","render":{"type":"object","eo:type":"object","properties":{"headers":{"type":"object","eo:type":"map","additionalProperties":{"type":"string","eo:type":"string"},"label":"请求头部"},"host":{"type":"string","eo:type":"string","switch":"host_rewrite===true","label":"Host"},"host_rewrite":{"type":"boolean","eo:type":"boolean","label":"是否重写host"},"not_match_err":{"type":"boolean","eo:type":"boolean","label":"path替换失败不进行转发"},"path_type":{"type":"string","eo:type":"string","enum":["none","static","prefix","regex"],"label":"path重写类型"},"prefix_path":{"type":"array","eo:type":"array","items":{"type":"object","eo:type":"object","properties":{"prefix_path_match":{"type":"string","eo:type":"string","label":"path前缀匹配字符串"},"prefix_path_replace":{"type":"string","eo:type":"string","label":"path前缀替换字符串"}},"ui:sort":["prefix_path_match","prefix_path_replace"]},"switch":"path_type==='prefix'","label":"path前缀替换"},"regex_path":{"type":"array","eo:type":"array","items":{"type":"object","eo:type":"object","properties":{"regex_path_match":{"type":"string","eo:type":"string","label":"path正则匹配表达式"},"regex_path_replace":{"type":"string","eo:type":"string","label":"path正则替换表达式"}},"ui:sort":["regex_path_match","regex_path_replace"]},"switch":"path_type==='regex'","label":"path正则替换"},"static_path":{"type":"string","eo:type":"string","switch":"path_type==='static'","label":"静态path"}},"ui:sort":["path_type","static_path","prefix_path","regex_path","not_match_err","host_rewrite","host","headers"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_rate_limiting.json b/resources/plugin/render/eolinker_com_apinto_rate_limiting.json new file mode 100644 index 00000000..c5a06ff4 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_rate_limiting.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"rate_limiting","version":"innert","render":{"type":"object","eo:type":"object","properties":{"day":{"type":"integer","eo:type":"integer","format":"int64","label":"每天请求次数限制"},"hide_client_header":{"type":"boolean","eo:type":"boolean","label":"是否隐藏流控信息"},"hour":{"type":"integer","eo:type":"integer","format":"int64","label":"每小时请求次数限制"},"minute":{"type":"integer","eo:type":"integer","format":"int64","label":"每分钟请求次数限制"},"response_type":{"type":"string","eo:type":"string","enum":["text","json"],"label":"报错格式"},"second":{"type":"integer","eo:type":"integer","format":"int64","label":"每秒请求次数限制"}},"ui:sort":["second","minute","hour","day","hide_client_header","response_type"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_request_file_parse.json b/resources/plugin/render/eolinker_com_apinto_request_file_parse.json new file mode 100644 index 00000000..b614cb1e --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_request_file_parse.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"request_file_parse","version":"innert","render":{"type":"object","eo:type":"object","properties":{"file_key":{"type":"string","eo:type":"string","label":"文件Key"},"file_suffix":{"type":"array","eo:type":"array","items":{"type":"string","eo:type":"string"},"label":"文件有效后缀列表"},"large_warn":{"type":"integer","eo:type":"integer","format":"int64","label":"文件大小警告阈值"},"large_warn_text":{"type":"string","eo:type":"string","label":"文件大小警告标签值"}},"ui:sort":["file_key","file_suffix","large_warn","large_warn_text"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_request_interception.json b/resources/plugin/render/eolinker_com_apinto_request_interception.json new file mode 100644 index 00000000..7140d82f --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_request_interception.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"request_interception","version":"innert","render":{"type":"object","eo:type":"object","properties":{"body":{"type":"string","eo:type":"string","label":"响应体"},"content_type":{"type":"string","eo:type":"string","enum":["text/plain","text/html","application/json"],"default":"application/json","label":"响应体类型"},"headers":{"type":"array","eo:type":"array","items":{"type":"object","eo:type":"object","properties":{"key":{"type":"string","eo:type":"string","label":"响应头Key"},"value":{"type":"array","eo:type":"array","items":{"type":"string","eo:type":"string"},"label":"响应头Value"}},"ui:sort":["key","value"]},"label":"响应头"},"status":{"type":"integer","eo:type":"integer","description":"最小值:100","format":"int32","default":200,"minimum":100,"label":"响应状态码"}},"ui:sort":["status","body","content_type","headers"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_response_file_parse.json b/resources/plugin/render/eolinker_com_apinto_response_file_parse.json new file mode 100644 index 00000000..4fdac790 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_response_file_parse.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"response_file_parse","version":"innert","render":{"type":"object","eo:type":"object","properties":{"file_key":{"type":"string","eo:type":"string","label":"文件Key"},"file_suffix":{"type":"array","eo:type":"array","items":{"type":"string","eo:type":"string"},"label":"文件有效后缀列表"},"large_warn":{"type":"integer","eo:type":"integer","format":"int64","label":"文件大小警告阈值"},"large_warn_text":{"type":"string","eo:type":"string","label":"文件大小警告标签值"}},"ui:sort":["file_key","file_suffix","large_warn","large_warn_text"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_response_filter.json b/resources/plugin/render/eolinker_com_apinto_response_filter.json new file mode 100644 index 00000000..fa3508e6 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_response_filter.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"response_filter","version":"innert","render":{"type":"object","eo:type":"object","properties":{"body_filter":{"type":"array","eo:type":"array","items":{"type":"string","eo:type":"string"},"label":"响应体过滤字段"},"header_filter":{"type":"array","eo:type":"array","items":{"type":"string","eo:type":"string"},"label":"响应头过滤字段"}},"ui:sort":["body_filter","header_filter"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_response_rewrite.json b/resources/plugin/render/eolinker_com_apinto_response_rewrite.json new file mode 100644 index 00000000..9cf20529 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_response_rewrite.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"response_rewrite","version":"innert","render":{"type":"object","eo:type":"object","properties":{"body":{"type":"string","eo:type":"string","label":"响应内容"},"body_base64":{"type":"boolean","eo:type":"boolean","label":"是否base64加密"},"headers":{"type":"object","eo:type":"map","additionalProperties":{"type":"string","eo:type":"string"},"label":"响应头部"},"match":{"type":"object","eo:type":"object","properties":{"code":{"type":"array","eo:type":"array","description":"最小值:100","items":{"type":"integer","eo:type":"integer","format":"int32"},"minimum":100,"label":"状态码"}},"ui:sort":["code"],"label":"匹配状态码列表"},"status_code":{"type":"integer","eo:type":"integer","description":"最小值:100","format":"int32","minimum":100,"label":"响应状态码"}},"ui:sort":["status_code","body","body_base64","headers","match"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_response_rewrite_v2.json b/resources/plugin/render/eolinker_com_apinto_response_rewrite_v2.json new file mode 100644 index 00000000..2bdda304 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_response_rewrite_v2.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"response_rewrite_v2","version":"innert","render":{"type":"object","eo:type":"object","properties":{"matches":{"type":"array","eo:type":"array","items":{"type":"object","eo:type":"object","properties":{"match_body":{"type":"object","eo:type":"object","properties":{"content":{"type":"string","eo:type":"string","label":"匹配内容"},"match_type":{"type":"string","eo:type":"string","enum":["equal","contain","prefix","suffix","regex"],"label":"匹配类型"}},"ui:sort":["content","match_type"],"label":"响应体匹配规则"},"match_headers":{"type":"array","eo:type":"array","items":{"type":"object","eo:type":"object","properties":{"content":{"type":"string","eo:type":"string","label":"匹配内容"},"header_key":{"type":"string","eo:type":"string","label":"响应头Key"},"match_type":{"type":"string","eo:type":"string","enum":["equal","contain","prefix","suffix","regex"],"label":"匹配类型"}},"ui:sort":["content","match_type","header_key"]},"label":"响应头匹配规则"},"match_status_code":{"type":"integer","eo:type":"integer","description":"最小值:100","format":"int32","default":200,"minimum":100,"label":"匹配状态码"},"response_rewrite":{"type":"object","eo:type":"object","properties":{"body":{"type":"string","eo:type":"string","label":"响应体"},"headers":{"type":"object","eo:type":"map","additionalProperties":{"type":"string","eo:type":"string"}},"status_code":{"type":"integer","eo:type":"integer","format":"int32","default":200,"label":"响应状态码"}},"ui:sort":["body","status_code","headers"],"label":"重写响应内容"}},"ui:sort":["match_status_code","match_headers","match_body","response_rewrite"]},"label":"响应匹配规则"}},"ui:sort":["matches"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_rsa.json b/resources/plugin/render/eolinker_com_apinto_rsa.json new file mode 100644 index 00000000..1860be41 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_rsa.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"rsa","version":"innert","render":{"type":"object","eo:type":"object","properties":{"format":{"type":"string","eo:type":"string","enum":["origin","base64"],"label":"密钥格式"},"private_key":{"type":"string","eo:type":"string","description":"对请求体进行解密,对响应体进行签名","label":"私钥"},"public_key":{"type":"string","eo:type":"string","description":"对请求体进行验签,对响应体进行加密","label":"公钥"},"request_sign_header":{"type":"string","eo:type":"string","label":"请求签名头"},"response_sign_header":{"type":"string","eo:type":"string","label":"响应签名头"}},"ui:sort":["private_key","public_key","request_sign_header","response_sign_header","format"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_strategy-plugin-cache.json b/resources/plugin/render/eolinker_com_apinto_strategy-plugin-cache.json new file mode 100644 index 00000000..75ddcef2 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_strategy-plugin-cache.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"strategy-plugin-cache","version":"innert","render":{"type":"object","eo:type":"object","properties":{"cache":{"type":"string","eo:type":"require","skill":"github.com/eolinker/apinto/resources.resources.ICache","label":"缓存位置"}},"ui:sort":["cache"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_strategy-plugin-fuse.json b/resources/plugin/render/eolinker_com_apinto_strategy-plugin-fuse.json new file mode 100644 index 00000000..5c89ca6d --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_strategy-plugin-fuse.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"strategy-plugin-fuse","version":"innert","render":{"type":"object","eo:type":"object","properties":{"cache":{"type":"string","eo:type":"require","skill":"github.com/eolinker/apinto/resources.resources.ICache","label":"缓存位置"}},"ui:sort":["cache"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_strategy-plugin-grey.json b/resources/plugin/render/eolinker_com_apinto_strategy-plugin-grey.json new file mode 100644 index 00000000..f7cdae4a --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_strategy-plugin-grey.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"strategy-plugin-grey","version":"innert","render":{"type":"object","eo:type":"object"}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_strategy-plugin-limiting.json b/resources/plugin/render/eolinker_com_apinto_strategy-plugin-limiting.json new file mode 100644 index 00000000..650fbc3c --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_strategy-plugin-limiting.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"strategy-plugin-limiting","version":"innert","render":{"type":"object","eo:type":"object","properties":{"cache":{"type":"string","eo:type":"require","skill":"github.com/eolinker/apinto/resources.resources.ICache","label":"缓存位置"}},"ui:sort":["cache"]}} \ No newline at end of file diff --git a/resources/plugin/render/eolinker_com_apinto_strategy-plugin-visit.json b/resources/plugin/render/eolinker_com_apinto_strategy-plugin-visit.json new file mode 100644 index 00000000..43dacca9 --- /dev/null +++ b/resources/plugin/render/eolinker_com_apinto_strategy-plugin-visit.json @@ -0,0 +1 @@ +{"group":"eolinker.com","project":"apinto","name":"strategy-plugin-visit","version":"innert","render":{"type":"object","eo:type":"object"}} \ No newline at end of file diff --git a/resources/plugin/render/render.init.sh b/resources/plugin/render/render.init.sh new file mode 100644 index 00000000..4af324e1 --- /dev/null +++ b/resources/plugin/render/render.init.sh @@ -0,0 +1,43 @@ +#!/bin/sh +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/access_log > eolinker_com_apinto_access_log.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/strategy-plugin-visit > eolinker_com_apinto_strategy-plugin-visit.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/plugin_app > eolinker_com_apinto_plugin_app.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/monitor > eolinker_com_apinto_monitor.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/http_to_dubbo2 > eolinker_com_apinto_http_to_dubbo2.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/proxy_rewrite > eolinker_com_apinto_proxy_rewrite.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/proxy_rewrite_v2 > eolinker_com_apinto_proxy_rewrite_v2.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/dubbo2-proxy-rewrite > eolinker_com_apinto_dubbo2-proxy-rewrite.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/dubbo2_to_http > eolinker_com_apinto_dubbo2_to_http.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/request_interception > eolinker_com_apinto_request_interception.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/response_rewrite > eolinker_com_apinto_response_rewrite.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/js_inject > eolinker_com_apinto_js_inject.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/request_file_parse > eolinker_com_apinto_request_file_parse.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/rate_limiting > eolinker_com_apinto_rate_limiting.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/acl > eolinker_com_apinto_acl.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/prometheus > eolinker_com_apinto_prometheus.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/response_filter > eolinker_com_apinto_response_filter.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/grpc_to_http > eolinker_com_apinto_grpc_to_http.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/http-mocking > eolinker_com_apinto_http-mocking.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/ip_restriction > eolinker_com_apinto_ip_restriction.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/cors > eolinker_com_apinto_cors.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/strategy-plugin-limiting > eolinker_com_apinto_strategy-plugin-limiting.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/extra_params > eolinker_com_apinto_extra_params.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/access_relational > eolinker_com_apinto_access_relational.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/data_transform > eolinker_com_apinto_data_transform.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/counter > eolinker_com_apinto_counter.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/grpc-proxy_write > eolinker_com_apinto_grpc-proxy_write.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/gzip > eolinker_com_apinto_gzip.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/strategy-plugin-cache > eolinker_com_apinto_strategy-plugin-cache.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/extra_params_v2 > eolinker_com_apinto_extra_params_v2.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/response_rewrite_v2 > eolinker_com_apinto_response_rewrite_v2.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/params_check > eolinker_com_apinto_params_check.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/circuit_breaker > eolinker_com_apinto_circuit_breaker.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/rsa > eolinker_com_apinto_rsa.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/http_to_grpc > eolinker_com_apinto_http_to_grpc.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/body_check > eolinker_com_apinto_body_check.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/params_transformer > eolinker_com_apinto_params_transformer.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/proxy_mirror > eolinker_com_apinto_proxy_mirror.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/oauth2 > eolinker_com_apinto_oauth2.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/strategy-plugin-grey > eolinker_com_apinto_strategy-plugin-grey.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/strategy-plugin-fuse > eolinker_com_apinto_strategy-plugin-fuse.json +curl -s http://127.0.0.1:9400/extender/eolinker.com:apinto/response_file_parse > eolinker_com_apinto_response_file_parse.json \ No newline at end of file diff --git a/scripts/Dockerfile b/scripts/Dockerfile new file mode 100755 index 00000000..cd08cc73 --- /dev/null +++ b/scripts/Dockerfile @@ -0,0 +1,18 @@ +# 名称:apinto通用镜像 +# 创建时间:2022-10-25 +FROM centos:7.9.2009 +MAINTAINER liujian + +ENV TZ=Asia/Shanghai +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +ARG APP + +RUN mkdir -p /${APP} + +COPY cmd/* /${APP}/ +COPY resource/* /${APP}/ + +WORKDIR /$APP +ENV ADMIN_PASSWORD=12345678 +CMD ./docker_run.sh \ No newline at end of file diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 00000000..47747583 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,136 @@ +#!/bin/bash + +cd "$(dirname "$0")/../" +LOCAL_PATH=$(pwd) + +source ./scripts/echo.sh +source ./scripts/common.sh +OUTPUT_DIR=$(mkdir_output "$1") +APP="apipark" +OUTPUT_BIN="${OUTPUT_DIR}/${APP}" +VERSION=$(gen_version "$2") +BUILD_TYPE=$3 +ARCH=$4 +if [[ $ARCH == "" ]];then + ARCH="amd64" +fi +echo ${VERSION} +set -e +version() { echo "$@" | awk -F. '{ printf("%d%03d%03d%03d\n", $1,$2,$3,$4); }'; } + +env_check() { + # 环境检查 + echo_info "Checking environment..." + + # 检查是否安装了 go + if ! command -v go &> /dev/null; then + echo_error "Go is not installed. Please install Go." + return 1 + fi + echo "Go is installed." + + # 获取Go的版本号 + go_version=$(go version | { read _ _ v _; echo ${v#go}; }) + + # 检查Go版本是否大于1.21 + if [ "$(version "${go_version}")" -lt "$(version 1.20)" ]; then + echo_error "Go version is less than 1.21. Please install Go version 1.21 or higher." + exit 1 + fi + echo "Go version is greater than 1.21." + + # 检查是否安装了 node + if ! command -v node &> /dev/null; then + echo_error "Node.js is not installed. Please install Node.js." + exit 1 + fi + echo "Node.js is installed." + + # 检查是否安装了npm + if ! command -v npm &> /dev/null; then + echo_error "Npm is not installed. Please install Npm." + exit 1 + fi + echo "Npm is installed." + +# # 检查是否安装了 pnpm +# if ! command -v pnpm &> /dev/null; then +# echo_error "Pnpm is not installed. Please install Pnpm." +# exit 1 +# fi +# echo "Pnpm is installed." + + # 如果所有检查都通过,打印环境检查通过的消息 + echo_info "All required tools are installed." +} + +# 打包前端,使用方式:build_frontend [build_type] +build_frontend() { + # 打包前端 + if [[ "$1" == "all" || ! -d "./frontend/dist" ]]; then + echo_info "Install dependencies..." + pnpm install --registry https://registry.npmmirror.com --dir ./frontend + echo_info "Build frontend..." + cd ./frontend && pnpm run build + cd .. + else + echo_info "Need not build frontend." + fi + return +} + +build_backend() { + # 打包后端 + echo_info "Build backend..." + flags="-X 'github.com/APIParkLab/APIPark/common/version.Version=${VERSION}' + -X 'github.com/APIParkLab/APIPark/common/version.goversion=$(go version)' + -X 'github.com/APIParkLab/APIPark/common/version.gitcommit=$(git rev-parse HEAD)' + -X 'github.com/APIParkLab/APIPark/common/version.BuildTime=$(date -u +"%Y-%m-%dT%H:%M:%SZ")' + -X 'github.com/APIParkLab/APIPark/common/version.builduser=$(id -u -n)'" + + Tags="" + if [ -n "$1" ]; then + Tags="--tags $1" + fi + + echo "go mod tidy" + go mod tidy + + if [ ! -d "${OUTPUT_DIR}" ];then + mkdir -p "${OUTPUT_DIR}" + fi + pwd + + # -ldflags="-w -s" means omit DWARF symbol table and the symbol table and debug information + echo "GOOS=linux GOARCH=$ARCH CGO_ENABLED=0 go build $Tags -ldflags \"-w -s $flags\" -o \"${OUTPUT_BIN}\"" + GOOS=linux GOARCH=$ARCH CGO_ENABLED=0 go build ${Tags} -ldflags "-w -s $flags" -o ${OUTPUT_BIN} + return +} + +package() { + # 打包 + echo_info "Package..." + PACKAGE_DIR="${OUTPUT_DIR}/${APP}_${VERSION}" + RESOURCE_DIR=./scripts/resource + + echo "mkdir -p ${PACKAGE_DIR}" + mkdir -p "${PACKAGE_DIR}" + + echo "cp -a ${RESOURCE_DIR}/* ${PACKAGE_DIR}" + cp -a ${RESOURCE_DIR}/* "${PACKAGE_DIR}" + + echo "cp ${OUTPUT_BIN} ${PACKAGE_DIR}" + + cp "${OUTPUT_BIN}" "${PACKAGE_DIR}" + + echo "tar -czvf ${PACKAGE_DIR}_linux_${ARCH}.tar.gz -C ${PACKAGE_DIR}/ ./" + tar -czvf "${PACKAGE_DIR}_linux_${ARCH}.tar.gz" -C "${PACKAGE_DIR}/" "./" +# rm -fr "${PACKAGE_DIR}" + echo_info "Package successfully..." +} + + +env_check +build_frontend "${BUILD_TYPE}" +build_backend "" +package "$1" \ No newline at end of file diff --git a/scripts/common.sh b/scripts/common.sh new file mode 100755 index 00000000..e334dd50 --- /dev/null +++ b/scripts/common.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# =========================================================================== +# File: common.sh +# Description: common functions +# Usage: . ./common.sh +# =========================================================================== + +gen_version() { + # 判断是否传参 + if [ -n "$1" ]; then + echo "$1" + return + fi + # 是否安装了 git + + tag=$(git describe --abbrev=0 --tags) + + if [ $? -ne 0 ]; then + tag=$(git rev-parse --short HEAD) + fi + + echo "${tag}" +} + +# Ensure output directory existed +mkdir_output() { + DEFAULT_OUTPUT_DIR="build" + if [ -z "$1" ]; then + OUTPUT_DIR=${DEFAULT_OUTPUT_DIR} + else + OUTPUT_DIR="$1" + fi + if [ ! -d "$OUTPUT_DIR" ]; then + mkdir -p "$OUTPUT_DIR" + fi + echo "$OUTPUT_DIR" + return +} \ No newline at end of file diff --git a/scripts/echo.sh b/scripts/echo.sh new file mode 100755 index 00000000..7d539f29 --- /dev/null +++ b/scripts/echo.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +echo_info() { + echo "\033[32m[INFO] $1 \033[0m" +} + +echo_error() { + echo "\033[31m[ERROR] $1 \033[0m" +} \ No newline at end of file diff --git a/scripts/resource/config.yml.tpl b/scripts/resource/config.yml.tpl new file mode 100644 index 00000000..d0dbc009 --- /dev/null +++ b/scripts/resource/config.yml.tpl @@ -0,0 +1,19 @@ +port: 8088 +mysql: + user_name: "数据库用户名" + password: "数据库密码" + ip: "数据库IP地址" + port: 端口号 + db: "数据库DB" +error_log: + dir: work/logs # 日志放置目录, 仅支持绝对路径, 不填则默认为执行程序上一层目录的work/logs. 若填写的值不为绝对路径,则以上一层目录为相对路径的根目录,比如填写 work/test/logs, 则目录为可执行程序所在目录的 ../work/test/logs + file_name: error.log # 错误日志文件名 + log_level: warning # 错误日志等级,可选:panic,fatal,error,warning,info,debug,trace 不填或者非法则为info + log_expire: 7d # 错误日志过期时间,默认单位为天,d|天,h|小时, 不合法配置默认为7d + log_period: day # 错误日志切割周期,仅支持day、hour +redis: + user_name: "redis集群密码" + password: "redis集群密码" + addr: + - 192.168.128.198:7201 + - 192.168.128.198:7202 \ No newline at end of file diff --git a/scripts/resource/docker_run.sh b/scripts/resource/docker_run.sh new file mode 100755 index 00000000..53d3d711 --- /dev/null +++ b/scripts/resource/docker_run.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +set -e + + +OLD_IFS="$IFS" +IFS="," +arr=(${REDIS_ADDR}) +IFS="$OLD_IFS" + + +echo "" > config.yml + +echo -e "mysql:" > config.yml +echo -e " user_name: ${MYSQL_USER_NAME}" >> config.yml +echo -e " password: ${MYSQL_PWD}" >> config.yml +echo -e " ip: ${MYSQL_IP}" >> config.yml +echo -e " port: ${MYSQL_PORT}" >> config.yml +echo -e " db: ${MYSQL_DB}" >> config.yml +echo -e "redis:" >> config.yml +echo -e " user_name: ${REDIS_USER_NAME}" >> config.yml +echo -e " password: ${REDIS_PWD}" >> config.yml +echo -e " addr: " >> config.yml +for s in ${arr[@]} +do +echo -e " - $s" >> config.yml +done +echo -e "port: 8288" >> config.yml +echo -e "error_log:" >> config.yml +echo -e " dir: ${ERROR_DIR}" >> config.yml +echo -e " file_name: ${ERROR_FILE_NAME}" >> config.yml +echo -e " log_level: ${ERROR_LOG_LEVEL}" >> config.yml +echo -e " log_expire: ${ERROR_EXPIRE}" >> config.yml +echo -e " log_period: ${ERROR_PERIOD}" >> config.yml + +cat config.yml +./apipark \ No newline at end of file diff --git a/scripts/resource/install.sh b/scripts/resource/install.sh new file mode 100755 index 00000000..0e04a6d4 --- /dev/null +++ b/scripts/resource/install.sh @@ -0,0 +1,54 @@ +#!/bin/sh + +set -e + +appName="apipark" + +OUTPUT_DIR="" +if [ -z "$1" ]; then + OUTPUT_DIR="/usr/local/${appName}" + echo "Use default directory /usr/local/${appName} as working directory y/n " + read reply leftover + case $reply in + y* | Y*) + mkdir -p ${OUTPUT_DIR} + + echo "create working directory success" + ;; + [nN]*) + exit 0;; + esac + +else + OUTPUT_DIR="$1" + mkdir -p ${OUTPUT_DIR} +fi + +echo "current installation directory ${OUTPUT_DIR}" + + +project_path=$(cd `dirname $0`; pwd) +project_name="${project_path##*/}" + +if [[ ${project_path} != ${OUTPUT_DIR}/${appName} && ! -d ${OUTPUT_DIR}/${project_name} ]]; then + mv ${project_path} ${OUTPUT_DIR} +fi + + +if [ ! -f ../config.yml ];then + echo "init config.yml ..." + cp config.yml.tpl ../config.yml + echo "init config.yml success" +fi + +mkdir -p ../work/logs + +ln -snf ../config.yml config.yml +ln -snf ../work work +ln -snf $project_name ../${appName} + +rm -rf ${OUTPUT_DIR}/${appName}/install.sh + +cd ${OUTPUT_DIR}/${appName} + +echo "install success" diff --git a/scripts/resource/run.sh b/scripts/resource/run.sh new file mode 100755 index 00000000..cc998bd9 --- /dev/null +++ b/scripts/resource/run.sh @@ -0,0 +1,95 @@ +#!/bin/sh + +if [ ! -d "work" ]; then + mkdir -p work +fi +APP="apipark" +APP_BIN="./${APP}" +DATE=$(date "+%Y-%m-%d %H:%M:%S") + +# 日志文件的路径 +LOG_FILE="work/${APP}-${DATE}.log" + +# PID 文件的路径,用于存储进程 ID +PID_FILE="work/${APP}.pid" + +is_program_running() { + # 使用 ps 命令查找程序,并通过 grep 过滤结果 + # -e 选项表示精确匹配,确保只找到完全匹配的进程 + if ps -ef | grep -v grep | grep -e $1 > /dev/null; then + return 0 # 程序正在运行 + else + return 1 # 程序没有运行 + fi +} + +# 启动函数 +start() { + # 创建新的日志文件 + date "+%Y-%m-%d %H:%M:%S" >> "$LOG_FILE" + echo "Starting ${APP}..." >> "$LOG_FILE" + + # 启动并重定向输出到日志文件 + # 使用 nohup 和 & 让程序在后台运行 + nohup "$APP_BIN" >> "$LOG_FILE" 2>&1 & + PID=$! + echo ${PID} > "$PID_FILE" + sleep 3 + if is_program_running ${PID}; then + echo "${APP} started with PID ${PID}, output is being logged to $LOG_FILE" + else + echo "${APP} failed to start, see $LOG_FILE for details" + cat "$LOG_FILE" + exit 1 + fi + +} + +# 停止函数 +stop() { + # 读取 PID 文件 + PID=$(cat "$PID_FILE") + + # 检查 PID 是否存在 + if [ -z "$PID" ]; then + echo "No ${APP} process is running." + return + fi + + # 发送 SIGTERM 信号以优雅地停止进程 + kill "$PID" + + # 确认进程是否已停止 + if kill -0 "$PID" 2>/dev/null; then + echo "${APP} is still running, attempting to force stop." + kill -9 "$PID" + else + echo "${APP} stopped successfully." + fi + + # 删除 PID 文件 + rm -f "$PID_FILE" +} + +# 主函数 +main() { + case "$1" in + start) + start + ;; + stop) + stop + ;; + restart) + stop + start + ;; + *) + echo "Usage: $0 {start|stop|restart}" + exit 1 + ;; + esac +} + +# 运行主函数 +main "$@" \ No newline at end of file diff --git a/service/api/iml.go b/service/api/iml.go new file mode 100644 index 00000000..5f1b45f4 --- /dev/null +++ b/service/api/iml.go @@ -0,0 +1,332 @@ +package api + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/google/uuid" + "gorm.io/gorm" + + "github.com/APIParkLab/APIPark/service/universally/commit" + + "github.com/APIParkLab/APIPark/stores/api" + + "github.com/eolinker/go-common/utils" + + "github.com/eolinker/go-common/auto" + + "github.com/APIParkLab/APIPark/service/universally" +) + +var ( + _ IAPIService = (*imlAPIService)(nil) +) + +type HistoryType string + +const ( + HistoryDocument HistoryType = "doc" + HistoryProxy HistoryType = "proxy" +) + +type imlAPIService struct { + store api.IApiBaseStore `autowired:""` + apiInfoStore api.IAPIInfoStore `autowired:""` + proxyCommitService commit.ICommitWithKeyService[Proxy] `autowired:""` + documentCommitService commit.ICommitWithKeyService[Document] `autowired:""` + universally.IServiceGet[API] + universally.IServiceDelete +} + +func (i *imlAPIService) CountMapByService(ctx context.Context, service ...string) (map[string]int64, error) { + w := map[string]interface{}{} + if len(service) > 0 { + w["service"] = service + } + return i.store.CountByGroup(ctx, "", w, "service") +} + +func (i *imlAPIService) ListInfoForService(ctx context.Context, serviceId string) ([]*Info, error) { + apis, err := i.store.List(ctx, map[string]interface{}{ + "service": serviceId, + }) + aids := utils.SliceToSlice(apis, func(a *api.Api) int64 { + return a.Id + }) + list, err := i.apiInfoStore.List(ctx, map[string]interface{}{ + "service": serviceId, + "id": aids, + }) + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, func(info *api.Info) *Info { + return &Info{ + UUID: info.UUID, + Name: info.Name, + Description: info.Description, + CreateAt: info.CreateAt, + UpdateAt: info.UpdateAt, + Service: info.Service, + Team: info.Team, + Creator: info.Creator, + Updater: info.Updater, + Method: info.Method, + Path: info.Path, + Match: info.Match, + } + }), nil +} + +func (i *imlAPIService) ListInfo(ctx context.Context, aids ...string) ([]*Info, error) { + list, err := i.apiInfoStore.List(ctx, map[string]interface{}{ + "uuid": aids, + }) + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, func(info *api.Info) *Info { + return &Info{ + UUID: info.UUID, + Name: info.Name, + Description: info.Description, + CreateAt: info.CreateAt, + UpdateAt: info.UpdateAt, + Service: info.Service, + Team: info.Team, + Creator: info.Creator, + Updater: info.Updater, + Method: info.Method, + Path: info.Path, + Match: info.Match, + } + }), nil +} + +func (i *imlAPIService) GetInfo(ctx context.Context, aid string) (*Info, error) { + + info, err := i.apiInfoStore.GetByUUID(ctx, aid) + if err != nil { + return nil, err + } + return &Info{ + UUID: info.UUID, + Name: info.Name, + Description: info.Description, + CreateAt: info.CreateAt, + UpdateAt: info.UpdateAt, + Service: info.Service, + Team: info.Team, + Creator: info.Creator, + Updater: info.Updater, + Method: info.Method, + Path: info.Path, + Match: info.Match, + }, nil +} + +func (i *imlAPIService) Save(ctx context.Context, id string, model *EditAPI) error { + if model == nil { + return errors.New("input is nil") + } + return i.apiInfoStore.Transaction(ctx, func(ctx context.Context) error { + ev, err := i.apiInfoStore.GetByUUID(ctx, id) + if err != nil { + return err + } + if model.Name != nil { + ev.Name = *model.Name + } + //if model.Upstream != nil { + // ev.Upstream = *model.Upstream + //} + if model.Description != nil { + ev.Description = *model.Description + } + e := i.apiInfoStore.Save(ctx, ev) + if e != nil { + return e + } + return i.store.SetLabels(ctx, ev.Id, getLabels(ev)...) + + }) +} +func getLabels(input *api.Info, appends ...string) []string { + labels := make([]string, 0, len(appends)+9) + labels = append(labels, input.UUID, input.Name, input.Description, input.Method, input.Path, input.Service, input.Team, input.Updater) + labels = append(labels, appends...) + return labels +} +func (i *imlAPIService) Create(ctx context.Context, input *CreateAPI) (err error) { + operater := utils.UserId(ctx) + return i.store.Transaction(ctx, func(ctx context.Context) error { + t, err := i.store.First(ctx, map[string]interface{}{ + "method": input.Method, + "path": input.Path, + }) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + } + + if t != nil { + return fmt.Errorf("method(%s),path(%s) is exist", input.Method, input.Path) + } + if input.UUID != "" { + a, err := i.store.GetByUUID(ctx, input.UUID) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if a != nil { + return fmt.Errorf("api(%s) is exist", input.UUID) + } + + } else { + input.UUID = uuid.NewString() + } + + ne := api.Api{ + Id: 0, + UUID: input.UUID, + Service: input.Service, + Team: input.Team, + Creator: operater, + CreateAt: time.Now(), + IsDelete: 0, + Method: input.Method, + Path: input.Path, + } + err = i.store.Insert(ctx, &ne) + if err != nil { + return err + } + ev := &api.Info{ + Id: ne.Id, + UUID: ne.UUID, + Name: input.Name, + Description: input.Description, + Updater: operater, + UpdateAt: time.Now(), + Creator: operater, + CreateAt: time.Now(), + //Upstream: input.Upstream, + Method: input.Method, + Path: input.Path, + Match: input.Match, + Service: input.Service, + Team: input.Team, + } + err = i.apiInfoStore.Save(ctx, ev) + if err != nil { + return err + } + err = i.store.SetLabels(ctx, ne.Id, getLabels(ev)...) + if err != nil { + return err + } + return nil + }) +} + +func (i *imlAPIService) ListProxyCommit(ctx context.Context, commitId ...string) ([]*commit.Commit[Proxy], error) { + return i.proxyCommitService.List(ctx, commitId...) +} + +func (i *imlAPIService) ListDocumentCommit(ctx context.Context, commitId ...string) ([]*commit.Commit[Document], error) { + return i.documentCommitService.List(ctx, commitId...) +} + +func (i *imlAPIService) CountByService(ctx context.Context, service string) (int64, error) { + return i.store.CountWhere(ctx, map[string]interface{}{ + "service": service, + }) +} + +func (i *imlAPIService) Exist(ctx context.Context, aid string, a *ExistAPI) error { + t, err := i.store.First(ctx, map[string]interface{}{ + "method": a.Method, + "path": a.Path, + }) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + return nil + } + if t.UUID != aid { + return fmt.Errorf("method(%s),path(%s) is exist", a.Method, a.Path) + } + return nil +} + +func (i *imlAPIService) ListForService(ctx context.Context, serviceId string) ([]*API, error) { + list, err := i.listForService(ctx, serviceId, false) + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromEntity), nil +} +func (i *imlAPIService) listForService(ctx context.Context, serviceId string, isDelete bool) ([]*api.Api, error) { + return i.store.ListQuery(ctx, "service=? and is_delete=?", []interface{}{serviceId, isDelete}, "id") +} +func (i *imlAPIService) ListLatestCommitProxy(ctx context.Context, apiUUID ...string) ([]*commit.Commit[Proxy], error) { + + return i.proxyCommitService.ListLatest(ctx, apiUUID...) + +} +func (i *imlAPIService) ListLatestCommitDocument(ctx context.Context, apiUUID ...string) ([]*commit.Commit[Document], error) { + + return i.documentCommitService.ListLatest(ctx, apiUUID...) +} + +func (i *imlAPIService) LatestProxy(ctx context.Context, aid string) (*commit.Commit[Proxy], error) { + + return i.proxyCommitService.Latest(ctx, aid) +} + +func (i *imlAPIService) LatestDocument(ctx context.Context, aid string) (*commit.Commit[Document], error) { + + return i.documentCommitService.Latest(ctx, aid) +} + +func (i *imlAPIService) GetProxyCommit(ctx context.Context, commitId string) (*commit.Commit[Proxy], error) { + return i.proxyCommitService.Get(ctx, commitId) +} + +func (i *imlAPIService) GetDocumentCommit(ctx context.Context, commitId string) (*commit.Commit[Document], error) { + return i.documentCommitService.Get(ctx, commitId) +} + +func (i *imlAPIService) SaveProxy(ctx context.Context, aid string, data *Proxy) error { + + return i.proxyCommitService.Save(ctx, aid, data) +} + +func (i *imlAPIService) SaveDocument(ctx context.Context, aid string, data *Document) error { + + return i.documentCommitService.Save(ctx, aid, data) +} + +func (i *imlAPIService) GetLabels(ctx context.Context, ids ...string) map[string]string { + if len(ids) == 0 { + return nil + } + list, err := i.apiInfoStore.ListQuery(ctx, "`uuid` in (?)", []interface{}{ids}, "id") + if err != nil { + return nil + } + return utils.SliceToMapO(list, func(i *api.Info) (string, string) { + return i.UUID, i.Name + }) +} + +func (i *imlAPIService) OnComplete() { + i.IServiceGet = universally.NewGetSoftDelete[API, api.Api](i.store, FromEntity) + + i.IServiceDelete = universally.NewSoftDelete[api.Api](i.store) + + auto.RegisterService("api", i) +} diff --git a/service/api/model.go b/service/api/model.go new file mode 100644 index 00000000..08f314c1 --- /dev/null +++ b/service/api/model.go @@ -0,0 +1,106 @@ +package api + +import ( + "time" + + "github.com/APIParkLab/APIPark/model/plugin_model" + + "github.com/APIParkLab/APIPark/stores/api" +) + +type API struct { + UUID string + Service string + Team string + Creator string + Method string + Path string + CreateAt time.Time + IsDelete bool +} + +type Info struct { + UUID string + Name string + Description string + CreateAt time.Time + UpdateAt time.Time + Service string + Team string + Creator string + Updater string + Upstream string + Method string + Path string + Match string +} + +func FromEntity(e *api.Api) *API { + return &API{ + UUID: e.UUID, + CreateAt: e.CreateAt, + IsDelete: e.IsDelete != 0, + Service: e.Service, + Team: e.Team, + Creator: e.Creator, + Method: e.Method, + Path: e.Path, + } +} + +type CreateAPI struct { + UUID string + Name string + Description string + Service string + Team string + Method string + Path string + Match string +} + +type EditAPI struct { + Name *string + Upstream *string + Description *string +} + +type ExistAPI struct { + Path string + Method string +} + +type Document struct { + Content string `json:"content"` +} +type PluginSetting struct { + Disable bool `json:"disable"` + Config plugin_model.ConfigType `json:"config"` +} +type Proxy struct { + Path string `json:"path"` + Timeout int `json:"timeout"` + Retry int `json:"retry"` + Plugins map[string]PluginSetting `json:"plugins"` + Extends map[string]any `json:"extends"` + Headers []*Header `json:"headers"` +} + +type Header struct { + Key string `json:"key"` + Value string `json:"value"` + Opt string `json:"opt"` +} + +type Router struct { + Method string `json:"method"` + Path string `json:"path"` + MatchRules []*Match `json:"match"` +} + +type Match struct { + Position string `json:"position"` + MatchType string `json:"match_type"` + Key string `json:"key"` + Pattern string `json:"pattern"` +} diff --git a/service/api/service.go b/service/api/service.go new file mode 100644 index 00000000..61c36231 --- /dev/null +++ b/service/api/service.go @@ -0,0 +1,48 @@ +package api + +import ( + "context" + "reflect" + + "github.com/APIParkLab/APIPark/service/universally/commit" + + "github.com/APIParkLab/APIPark/service/universally" + "github.com/eolinker/go-common/autowire" +) + +type IAPIService interface { + universally.IServiceGet[API] + universally.IServiceDelete + CountByService(ctx context.Context, service string) (int64, error) + CountMapByService(ctx context.Context, service ...string) (map[string]int64, error) + Exist(ctx context.Context, aid string, api *ExistAPI) error + ListForService(ctx context.Context, serviceId string) ([]*API, error) + GetInfo(ctx context.Context, aid string) (*Info, error) + ListInfo(ctx context.Context, aids ...string) ([]*Info, error) + ListInfoForService(ctx context.Context, serviceId string) ([]*Info, error) + ListLatestCommitProxy(ctx context.Context, aid ...string) ([]*commit.Commit[Proxy], error) + ListLatestCommitDocument(ctx context.Context, aid ...string) ([]*commit.Commit[Document], error) + LatestProxy(ctx context.Context, aid string) (*commit.Commit[Proxy], error) + LatestDocument(ctx context.Context, aid string) (*commit.Commit[Document], error) + GetProxyCommit(ctx context.Context, commitId string) (*commit.Commit[Proxy], error) + ListProxyCommit(ctx context.Context, commitId ...string) ([]*commit.Commit[Proxy], error) + GetDocumentCommit(ctx context.Context, commitId string) (*commit.Commit[Document], error) + ListDocumentCommit(ctx context.Context, commitId ...string) ([]*commit.Commit[Document], error) + SaveProxy(ctx context.Context, aid string, data *Proxy) error + SaveDocument(ctx context.Context, aid string, data *Document) error + Save(ctx context.Context, id string, model *EditAPI) error + Create(ctx context.Context, input *CreateAPI) (err error) +} + +var ( + _ IAPIService = (*imlAPIService)(nil) +) + +func init() { + autowire.Auto[IAPIService](func() reflect.Value { + return reflect.ValueOf(new(imlAPIService)) + }) + + commit.InitCommitWithKeyService[Proxy]("api", string(HistoryProxy)) + commit.InitCommitWithKeyService[Document]("api", string(HistoryDocument)) +} diff --git a/service/application-authorization/iml.go b/service/application-authorization/iml.go new file mode 100644 index 00000000..97feab6f --- /dev/null +++ b/service/application-authorization/iml.go @@ -0,0 +1,106 @@ +package application_authorization + +import ( + "context" + "time" + + "github.com/eolinker/go-common/utils" + + "github.com/eolinker/go-common/auto" + + "github.com/APIParkLab/APIPark/service/universally" + "github.com/APIParkLab/APIPark/stores/service" +) + +var ( + _ IAuthorizationService = (*imlAuthorizationService)(nil) +) + +type imlAuthorizationService struct { + store service.IAuthorizationStore `autowired:""` + universally.IServiceGet[Authorization] + universally.IServiceDelete + universally.IServiceCreate[Create] + universally.IServiceEdit[Edit] +} + +func (i *imlAuthorizationService) ListByApp(ctx context.Context, appId ...string) ([]*Authorization, error) { + w := map[string]interface{}{} + if len(appId) > 0 { + w["application"] = appId + } + list, err := i.store.List(ctx, w, "update_at desc") + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromEntity), nil +} + +func (i *imlAuthorizationService) GetLabels(ctx context.Context, ids ...string) map[string]string { + if len(ids) == 0 { + return nil + } + list, err := i.store.ListQuery(ctx, "`uuid` in (?)", []interface{}{ids}, "id") + if err != nil { + return nil + } + return utils.SliceToMapO(list, func(i *service.Authorization) (string, string) { + return i.UUID, i.Name + }) +} + +func (i *imlAuthorizationService) OnComplete() { + i.IServiceGet = universally.NewGet[Authorization, service.Authorization](i.store, FromEntity) + + i.IServiceDelete = universally.NewDelete[service.Authorization](i.store) + + i.IServiceCreate = universally.NewCreator[Create, service.Authorization](i.store, "project_authorization", createEntityHandler, uniquestHandler, labelHandler) + + i.IServiceEdit = universally.NewEdit[Edit, service.Authorization](i.store, updateHandler, labelHandler) + auto.RegisterService("project_authorization", i) +} + +func labelHandler(e *service.Authorization) []string { + return []string{e.Name, e.UUID} +} +func uniquestHandler(i *Create) []map[string]interface{} { + return []map[string]interface{}{{"uuid": i.UUID}} +} +func createEntityHandler(i *Create) *service.Authorization { + now := time.Now() + return &service.Authorization{ + UUID: i.UUID, + Name: i.Name, + Application: i.Application, + Type: i.Type, + Position: i.Position, + TokenName: i.TokenName, + Config: i.Config, + ExpireTime: i.ExpireTime, + CreateAt: now, + UpdateAt: now, + HideCredential: i.HideCredential, + } +} + +func updateHandler(e *service.Authorization, i *Edit) { + if i.Name != nil { + e.Name = *i.Name + } + if i.Position != nil { + e.Position = *i.Position + } + if i.TokenName != nil { + e.TokenName = *i.TokenName + } + if i.Config != nil { + e.Config = *i.Config + } + if i.ExpireTime != nil { + e.ExpireTime = *i.ExpireTime + } + if i.HideCredential != nil { + e.HideCredential = *i.HideCredential + } + e.UpdateAt = time.Now() +} diff --git a/service/application-authorization/model.go b/service/application-authorization/model.go new file mode 100644 index 00000000..138e862f --- /dev/null +++ b/service/application-authorization/model.go @@ -0,0 +1,64 @@ +package application_authorization + +import ( + "time" + + "github.com/APIParkLab/APIPark/stores/service" +) + +type Authorization struct { + UUID string + Application string + Name string + Type string + Position string + TokenName string + Config string + Creator string + Updater string + CreateTime time.Time + UpdateTime time.Time + ExpireTime int64 + HideCredential bool +} + +func FromEntity(e *service.Authorization) *Authorization { + return &Authorization{ + UUID: e.UUID, + Application: e.Application, + Name: e.Name, + Type: e.Type, + Position: e.Position, + TokenName: e.TokenName, + Config: e.Config, + Creator: e.Creator, + Updater: e.Updater, + CreateTime: e.CreateAt, + UpdateTime: e.UpdateAt, + ExpireTime: e.ExpireTime, + HideCredential: e.HideCredential, + } +} + +type Create struct { + UUID string + Application string + Name string + Type string + Position string + TokenName string + Config string + AuthID string + ExpireTime int64 + HideCredential bool +} + +type Edit struct { + Name *string + Position *string + TokenName *string + Config *string + ExpireTime *int64 + HideCredential *bool + AuthID *string +} diff --git a/service/application-authorization/service.go b/service/application-authorization/service.go new file mode 100644 index 00000000..cb355bb9 --- /dev/null +++ b/service/application-authorization/service.go @@ -0,0 +1,23 @@ +package application_authorization + +import ( + "context" + "reflect" + + "github.com/APIParkLab/APIPark/service/universally" + "github.com/eolinker/go-common/autowire" +) + +type IAuthorizationService interface { + universally.IServiceGet[Authorization] + universally.IServiceDelete + universally.IServiceCreate[Create] + universally.IServiceEdit[Edit] + ListByApp(ctx context.Context, appId ...string) ([]*Authorization, error) +} + +func init() { + autowire.Auto[IAuthorizationService](func() reflect.Value { + return reflect.ValueOf(new(imlAuthorizationService)) + }) +} diff --git a/service/catalogue/iml.go b/service/catalogue/iml.go new file mode 100644 index 00000000..3989bae8 --- /dev/null +++ b/service/catalogue/iml.go @@ -0,0 +1,71 @@ +package catalogue + +import ( + "context" + "time" + + "github.com/eolinker/go-common/auto" + + "github.com/eolinker/go-common/utils" + + "github.com/APIParkLab/APIPark/service/universally" + "github.com/APIParkLab/APIPark/stores/catalogue" +) + +var ( + _ ICatalogueService = (*imlCatalogueService)(nil) +) + +type imlCatalogueService struct { + store catalogue.ICatalogueStore `autowired:""` + universally.IServiceGet[Catalogue] + universally.IServiceDelete + universally.IServiceCreate[CreateCatalogue] + universally.IServiceEdit[EditCatalogue] +} + +func (i *imlCatalogueService) OnComplete() { + i.IServiceGet = universally.NewGet[Catalogue, catalogue.Catalogue](i.store, FromEntity) + i.IServiceCreate = universally.NewCreator[CreateCatalogue, catalogue.Catalogue](i.store, "catalogue", createEntityHandler, uniquestHandler, labelHandler) + i.IServiceEdit = universally.NewEdit[EditCatalogue, catalogue.Catalogue](i.store, updateHandler, labelHandler) + i.IServiceDelete = universally.NewDelete[catalogue.Catalogue](i.store) + auto.RegisterService("catalogue", i) +} + +func (i *imlCatalogueService) GetLabels(ctx context.Context, ids ...string) map[string]string { + catalogues, err := i.store.ListQuery(ctx, "uuid in(?)", []interface{}{ids}, "id") + if err != nil { + return map[string]string{} + } + return utils.SliceToMapO(catalogues, func(t *catalogue.Catalogue) (string, string) { + return t.UUID, t.Name + }) +} + +func labelHandler(e *catalogue.Catalogue) []string { + return []string{e.Name, e.UUID} +} +func uniquestHandler(i *CreateCatalogue) []map[string]interface{} { + return []map[string]interface{}{{"uuid": i.Id}} +} +func createEntityHandler(i *CreateCatalogue) *catalogue.Catalogue { + return &catalogue.Catalogue{ + UUID: i.Id, + Name: i.Name, + Parent: i.Parent, + CreateAt: time.Now(), + UpdateAt: time.Now(), + } +} +func updateHandler(e *catalogue.Catalogue, i *EditCatalogue) { + if i.Name != nil { + e.Name = *i.Name + } + if i.Parent != nil { + e.Parent = *i.Parent + } + if i.Sort != nil { + e.Sort = *i.Sort + } + e.UpdateAt = time.Now() +} diff --git a/service/catalogue/model.go b/service/catalogue/model.go new file mode 100644 index 00000000..23a88e42 --- /dev/null +++ b/service/catalogue/model.go @@ -0,0 +1,44 @@ +package catalogue + +import ( + "time" + + "github.com/APIParkLab/APIPark/stores/catalogue" +) + +type Catalogue struct { + // 目录ID + Id string + // 名称 + Name string + // 父目录ID + Parent string + Sort int + CreateTime time.Time + UpdateTime time.Time +} + +func FromEntity(e *catalogue.Catalogue) *Catalogue { + return &Catalogue{ + Id: e.UUID, + Name: e.Name, + Parent: e.Parent, + Sort: e.Sort, + CreateTime: e.CreateAt, + UpdateTime: e.UpdateAt, + } +} + +type CreateCatalogue struct { + Id string + Name string + Parent string + Sort int +} + +type EditCatalogue struct { + Id *string + Name *string + Parent *string + Sort *int +} diff --git a/service/catalogue/service.go b/service/catalogue/service.go new file mode 100644 index 00000000..64ca15a4 --- /dev/null +++ b/service/catalogue/service.go @@ -0,0 +1,21 @@ +package catalogue + +import ( + "reflect" + + "github.com/APIParkLab/APIPark/service/universally" + "github.com/eolinker/go-common/autowire" +) + +type ICatalogueService interface { + universally.IServiceGet[Catalogue] + universally.IServiceDelete + universally.IServiceCreate[CreateCatalogue] + universally.IServiceEdit[EditCatalogue] +} + +func init() { + autowire.Auto[ICatalogueService](func() reflect.Value { + return reflect.ValueOf(new(imlCatalogueService)) + }) +} diff --git a/service/certificate/model.go b/service/certificate/model.go new file mode 100644 index 00000000..4925e1df --- /dev/null +++ b/service/certificate/model.go @@ -0,0 +1,20 @@ +package certificate + +import "time" + +type Certificate struct { + ID string `json:"id"` + Name string `json:"name"` + Domains []string `json:"domains"` + Cluster string `json:"cluster"` + NotBefore time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:not_before;comment:生效时间"` + NotAfter time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:not_after;comment:失效时间"` + Updater string `json:"updater"` + UpdateTime time.Time `json:"update_time"` +} + +type File struct { + ID string `json:"id"` + Key []byte `json:"key"` + Cert []byte `json:"cert"` +} diff --git a/service/certificate/service.go b/service/certificate/service.go new file mode 100644 index 00000000..9509b29d --- /dev/null +++ b/service/certificate/service.go @@ -0,0 +1,224 @@ +package certificate + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "errors" + "reflect" + "time" + + "github.com/APIParkLab/APIPark/stores/certificate" + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/utils" + "github.com/google/uuid" +) + +var ( + _ ICertificateService = (*imlCertificateService)(nil) +) + +func init() { + autowire.Auto[ICertificateService](func() reflect.Value { + return reflect.ValueOf(new(imlCertificateService)) + }) +} + +type ICertificateService interface { + Get(ctx context.Context, id string) (*Certificate, *File, error) + List(ctx context.Context, clusterId string) ([]*Certificate, error) + Save(ctx context.Context, id, clusterId string, key, cert []byte) (*Certificate, error) + Delete(ctx context.Context, id string) error +} + +type imlCertificateService struct { + store certificate.ICertificateStore `autowired:""` + file certificate.ICertificateFileStore `autowired:""` +} + +func (s *imlCertificateService) Delete(ctx context.Context, id string) error { + return s.store.Transaction(ctx, func(ctx context.Context) error { + i, err := s.store.DeleteWhere(ctx, map[string]interface{}{"uuid": id}) + if err != nil { + return err + } + if i == 0 { + return nil + } + _, err = s.file.DeleteWhere(ctx, map[string]interface{}{"uuid": id}) + if err != nil { + return err + } + return nil + + }) + +} + +func (s *imlCertificateService) Get(ctx context.Context, id string) (*Certificate, *File, error) { + ce, err := s.store.First(ctx, map[string]interface{}{"uuid": id}) + if err != nil { + return nil, nil, err + } + fe, err := s.file.Get(ctx, ce.Id) + if err != nil { + return nil, nil, err + } + return &Certificate{ + ID: ce.UUID, + Cluster: ce.Cluster, + UpdateTime: ce.UpdateTime, + Name: ce.Name, + Domains: ce.Domains, + NotAfter: ce.NotAfter, + NotBefore: ce.NotBefore, + Updater: ce.Updater, + }, &File{ + ID: id, + Key: fe.Key, + Cert: fe.Cert, + }, nil + +} + +func (s *imlCertificateService) List(ctx context.Context, clusterId string) ([]*Certificate, error) { + list, err := s.store.List(ctx, map[string]interface{}{"cluster": clusterId}) + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, func(i *certificate.Certificate) *Certificate { + return &Certificate{ + ID: i.UUID, + Name: i.Name, + Domains: i.Domains, + Cluster: i.Cluster, + NotAfter: i.NotAfter, + NotBefore: i.NotBefore, + Updater: i.Updater, + UpdateTime: i.UpdateTime, + } + }), nil +} + +func (s *imlCertificateService) Save(ctx context.Context, id, clusterId string, key, cert []byte) (*Certificate, error) { + if id == "" { + id = uuid.NewString() + } + operator := utils.UserId(ctx) + certDERBlock, err := ParseCert(string(key), string(cert)) + if err != nil { + return nil, err + } + dnsNames := certDERBlock.Leaf.DNSNames + if dnsNames == nil && certDERBlock.Leaf.IPAddresses != nil { + dnsNames = make([]string, 0, len(certDERBlock.Leaf.IPAddresses)) + for _, ip := range certDERBlock.Leaf.IPAddresses { + dnsNames = append(dnsNames, ip.String()) + } + } + if dnsNames == nil { + return nil, errors.New("证书中没有包含域名或者IP地址信息") + } + ce := &certificate.Certificate{ + UUID: id, + Cluster: clusterId, + Name: certDERBlock.Leaf.Subject.CommonName, + Domains: dnsNames, + Updater: operator, + NotAfter: certDERBlock.Leaf.NotAfter, + NotBefore: certDERBlock.Leaf.NotBefore, + UpdateTime: time.Now(), + } + fe := &certificate.File{ + UUID: id, + Key: key, + Cert: cert, + } + err = s.store.Transaction(ctx, func(ctx context.Context) error { + err := s.store.Save(ctx, ce) + if err != nil { + return err + } + fe.Id = ce.Id + return s.file.Save(ctx, fe) + + }) + if err != nil { + return nil, err + } + + return &Certificate{ + ID: ce.UUID, + Name: ce.Name, + Domains: ce.Domains, + Cluster: ce.Cluster, + NotAfter: ce.NotAfter, + NotBefore: ce.NotBefore, + Updater: ce.Updater, + UpdateTime: ce.UpdateTime, + }, nil + +} + +//func parseCert(crt []byte) (*x509.Certificate, error) { +// +// //获取下一个pem格式证书数据 -----BEGIN CERTIFICATE----- -----END CERTIFICATE----- +// certDERBlock, _ := pem.Decode(crt) +// if certDERBlock == nil { +// return nil, fmt.Errorf("pem.Decode failed") +// } +// +// //第一个叶子证书就是我们https中使用的证书 +// x509Cert, err := x509.ParseCertificate(certDERBlock.Bytes) +// if err != nil { +// +// return nil, fmt.Errorf("x509.ParseCertificate failed:%w", err) +// } +// return x509Cert, nil +//} + +func ParseCert(privateKey, pemValue string) (*tls.Certificate, error) { + var cert tls.Certificate + //获取下一个pem格式证书数据 -----BEGIN CERTIFICATE----- -----END CERTIFICATE----- + certDERBlock, restPEMBlock := pem.Decode([]byte(pemValue)) + if certDERBlock == nil { + return nil, errors.New("证书解析失败") + } + //附加数字证书到返回 + cert.Certificate = append(cert.Certificate, certDERBlock.Bytes) + //继续解析Certificate Chan,这里要明白证书链的概念 + certDERBlockChain, _ := pem.Decode(restPEMBlock) + if certDERBlockChain != nil { + //追加证书链证书到返回 + cert.Certificate = append(cert.Certificate, certDERBlockChain.Bytes) + } + + //解码pem格式的私钥------BEGIN RSA PRIVATE KEY----- -----END RSA PRIVATE KEY----- + keyDERBlock, _ := pem.Decode([]byte(privateKey)) + if keyDERBlock == nil { + return nil, errors.New("证书解析失败") + } + var key interface{} + var errParsePK error + if keyDERBlock.Type == "RSA PRIVATE KEY" { + //RSA PKCS1 + key, errParsePK = x509.ParsePKCS1PrivateKey(keyDERBlock.Bytes) + } else if keyDERBlock.Type == "PRIVATE KEY" { + //pkcs8格式的私钥解析 + key, errParsePK = x509.ParsePKCS8PrivateKey(keyDERBlock.Bytes) + } + + if errParsePK != nil { + return nil, errors.New("证书解析失败") + } else { + cert.PrivateKey = key + } + //第一个叶子证书就是我们https中使用的证书 + x509Cert, err := x509.ParseCertificate(certDERBlock.Bytes) + if err != nil { + return nil, err + } + cert.Leaf = x509Cert + return &cert, nil +} diff --git a/service/cluster/cluster.go b/service/cluster/cluster.go new file mode 100644 index 00000000..824d6c95 --- /dev/null +++ b/service/cluster/cluster.go @@ -0,0 +1,439 @@ +package cluster + +import ( + "context" + "errors" + "reflect" + "strings" + "time" + + "github.com/APIParkLab/APIPark/gateway" + "github.com/APIParkLab/APIPark/gateway/admin" + "github.com/APIParkLab/APIPark/stores/cluster" + "github.com/eolinker/go-common/auto" + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/utils" + "gorm.io/gorm" +) + +var ( + _ IClusterService = (*imlClusterService)(nil) + _ auto.CompleteService = (*imlClusterService)(nil) + DefaultClusterID = "default" +) + +type IClusterService interface { + CountByPartition(ctx context.Context) (map[string]int, error) + List(ctx context.Context, clusterIds ...string) ([]*Cluster, error) + ListByClusters(ctx context.Context, ids ...string) ([]*Cluster, error) + Search(ctx context.Context, keyword string, clusterId ...string) ([]*Cluster, error) + Create(ctx context.Context, name string, resume string, address string) (*Cluster, error) + UpdateInfo(ctx context.Context, id string, name *string, resume *string) (*Cluster, error) + UpdateAddress(ctx context.Context, id string, address string) ([]*Node, error) + Nodes(ctx context.Context, clusterIds ...string) ([]*Node, error) + GatewayClient(ctx context.Context, id string) (gateway.IClientDriver, error) + Get(ctx context.Context, id string) (*Cluster, error) + Delete(ctx context.Context, id string) error +} + +type imlClusterService struct { + store cluster.IClusterStore `autowired:""` + nodeStore cluster.IClusterNodeStore `autowired:""` + nodeAddressStore cluster.IClusterNodeAddressStore `autowired:""` +} + +func (s *imlClusterService) GatewayClient(ctx context.Context, id string) (gateway.IClientDriver, error) { + nodes, err := s.Nodes(ctx, id) + if err != nil { + return nil, err + } + address := make([]string, 0, len(nodes)) + for _, n := range nodes { + address = append(address, n.Admin...) + } + return gateway.GetClient("apinto", &gateway.ClientConfig{ + Addresses: address, + }) +} + +func (s *imlClusterService) ListByClusters(ctx context.Context, ids ...string) ([]*Cluster, error) { + wm := make(map[string]interface{}) + + if len(ids) > 0 { + wm["uuid"] = ids + } + list, err := s.store.List(ctx, wm, "update_at desc") + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromEntity), nil +} + +func (s *imlClusterService) GetLabels(ctx context.Context, ids ...string) map[string]string { + if len(ids) == 0 { + return nil + } + if len(ids) == 1 { + o, err := s.store.GetByUUID(ctx, ids[0]) + if err != nil || o == nil { + return nil + } + return map[string]string{o.UUID: o.Name} + } + list, err := s.store.ListQuery(ctx, "uuid in ?", []interface{}{ids}, "id") + if err != nil { + return nil + } + return utils.SliceToMapO(list, func(o *cluster.Cluster) (string, string) { return o.UUID, o.Name }) +} + +func (s *imlClusterService) OnComplete() { + auto.RegisterService("cluster", s) +} + +func (s *imlClusterService) Delete(ctx context.Context, id string) error { + return s.store.Transaction(ctx, func(ctx context.Context) error { + _, err := s.store.DeleteWhere(ctx, map[string]interface{}{ + "uuid": id, + }) + if err != nil { + return err + } + _, err = s.nodeStore.DeleteWhere(ctx, map[string]interface{}{ + "cluster": id, + }) + if err != nil { + return err + } + _, err = s.nodeAddressStore.DeleteWhere(ctx, map[string]interface{}{ + "cluster": id, + }) + if err != nil { + return err + } + return nil + }) +} + +func (s *imlClusterService) Get(ctx context.Context, id string) (*Cluster, error) { + v, err := s.store.FirstQuery(ctx, "`uuid` = ?", []interface{}{id}, "id desc") + if err != nil { + return nil, err + } + return FromEntity(v), nil +} + +func (s *imlClusterService) Create(ctx context.Context, name string, resume string, address string) (*Cluster, error) { + apintoInfo, err := admin.Admin(address).Info(ctx) + if err != nil { + return nil, err + } + operator := utils.UserId(ctx) + + // check cluster + query, err := s.store.FirstQuery(ctx, "`uuid` = ?", []interface{}{apintoInfo.Cluster}, "id desc") + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + if query != nil { + return nil, errors.New("cluster already exists") + } + // check node + nodeIds := utils.SliceToSlice(apintoInfo.Nodes, func(i *admin.Node) string { + return i.Id + }) + nodeNames := utils.SliceToSlice(apintoInfo.Nodes, func(i *admin.Node) string { + return i.Name + }) + + nodeExist, err := s.nodeStore.FirstQuery(ctx, "`uuid` in (?) or `name` in (?)", []interface{}{nodeIds, nodeNames}, "id desc") + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + if nodeExist != nil { + return nil, errors.New("node already exists") + } + + en := &cluster.Cluster{ + Id: 0, + UUID: apintoInfo.Cluster, + Name: name, + Resume: resume, + Creator: operator, + Updater: operator, + CreateAt: time.Now(), + UpdateAt: time.Now(), + } + nodeEn, addrEn := s.genNodeEntity(apintoInfo.Cluster, apintoInfo.Nodes) + err = s.store.Transaction(ctx, func(ctx context.Context) error { + + err := s.store.Insert(ctx, en) + if err != nil { + return err + } + err = s.nodeStore.Insert(ctx, nodeEn...) + if err != nil { + return err + } + err = s.nodeAddressStore.Insert(ctx, addrEn...) + if err != nil { + return err + } + return nil + }) + if err != nil { + return nil, err + } + return FromEntity(en), nil +} +func (s *imlClusterService) genNodeEntity(id string, nodes []*admin.Node) ([]*cluster.Node, []*cluster.NodeAddr) { + nodeEn := make([]*cluster.Node, 0, len(nodes)) + addrAllTemp := make([][]*cluster.NodeAddr, 0, len(nodes)) + now := time.Now() + for _, node := range nodes { + nodeEn = append(nodeEn, &cluster.Node{ + Id: 0, + UUID: node.Id, + Name: node.Name, + Cluster: id, + UpdateTime: now, + }) + aden := make([]*cluster.NodeAddr, 0, len(node.Peer)+len(node.Admin)+len(node.Server)) + for _, addr := range node.Peer { + aden = append(aden, &cluster.NodeAddr{ + Id: 0, + Cluster: id, + Node: node.Id, + Type: "peer", + Addr: addr, + UpdateTime: now, + }) + } + for _, addr := range node.Admin { + aden = append(aden, &cluster.NodeAddr{ + Id: 0, + Cluster: id, + Node: node.Id, + Type: "admin", + Addr: addr, + UpdateTime: now, + }) + } + for _, addr := range node.Server { + aden = append(aden, &cluster.NodeAddr{ + Id: 0, + Cluster: id, + Node: node.Id, + Type: "server", + Addr: addr, + UpdateTime: now, + }) + } + addrAllTemp = append(addrAllTemp, aden) + } + + return nodeEn, utils.SliceMerge(addrAllTemp) + +} +func (s *imlClusterService) UpdateInfo(ctx context.Context, id string, name *string, resume *string) (c *Cluster, errOut error) { + operator := utils.UserId(ctx) + if name == nil && resume == nil { + return nil, errors.New("no update") + } + errOut = s.store.Transaction(ctx, func(ctx context.Context) error { + v, err := s.store.FirstQuery(ctx, "`uuid` = ?", []interface{}{id}, "id desc") + if err != nil { + return err + } + if name != nil { + v.Name = *name + } + if resume != nil { + v.Resume = *resume + } + v.Updater = operator + v.UpdateAt = time.Now() + upCount, err := s.store.Update(ctx, v) + if err != nil { + return err + } + if upCount == 0 { + return errors.New("no update") + } + c = FromEntity(v) + return nil + }) + + return + +} + +func (s *imlClusterService) UpdateAddress(ctx context.Context, id string, address string) ([]*Node, error) { + + info, err := admin.Admin(address).Info(ctx) + if err != nil { + return nil, err + } + //if info.Cluster != id { + // return nil, errors.New("cluster id not match") + //} + operator := utils.UserId(ctx) + now := time.Now() + cv, err := s.store.FirstQuery(ctx, "`uuid` = ?", []interface{}{id}, "id desc") + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + cv = &cluster.Cluster{ + UUID: id, + Name: "默认集群", + Resume: "默认集群", + Creator: operator, + CreateAt: now, + } + } + + // check node + nodeIds := utils.SliceToSlice(info.Nodes, func(i *admin.Node) string { + return i.Id + }) + nodeNames := utils.SliceToSlice(info.Nodes, func(i *admin.Node) string { + return i.Name + }) + + nodeExist, err := s.nodeStore.FirstQuery(ctx, "`cluster` = ? and (`uuid` in (?) or `name` in (?))", []interface{}{id, nodeIds, nodeNames}, "id desc") + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + if nodeExist != nil && id != nodeExist.Cluster { + return nil, errors.New("node already exists") + } + s.genNodeEntity(id, info.Nodes) + nodeEn, addrEn := s.genNodeEntity(id, info.Nodes) + err = s.store.Transaction(ctx, func(ctx context.Context) error { + _, err := s.nodeStore.DeleteWhere(ctx, map[string]interface{}{ + "cluster": id, + }) + if err != nil { + return err + } + _, err = s.nodeAddressStore.DeleteWhere(ctx, map[string]interface{}{ + "cluster": id, + }) + if err != nil { + return err + } + cv.Updater = operator + cv.UpdateAt = now + err = s.store.Save(ctx, cv) + if err != nil { + return err + } + //if uc == 0 { + // return errors.New("no update") + //} + err = s.nodeStore.Insert(ctx, nodeEn...) + if err != nil { + return err + } + err = s.nodeAddressStore.Insert(ctx, addrEn...) + if err != nil { + return err + } + return nil + }) + if err != nil { + return nil, err + } + return s.Nodes(ctx, id) + +} + +func (s *imlClusterService) Nodes(ctx context.Context, clusterIds ...string) ([]*Node, error) { + w := make(map[string]interface{}) + if len(clusterIds) > 0 { + w["cluster"] = clusterIds + } + nodeAddrs, err := s.nodeAddressStore.List(ctx, w, "id desc") + if err != nil { + return nil, err + } + nodes, err := s.nodeStore.List(ctx, w, "id desc") + if err != nil { + return nil, err + } + + addrOfNode := utils.SliceToMapArray(nodeAddrs, func(i *cluster.NodeAddr) string { + return i.Node + }) + + return utils.SliceToSlice(nodes, func(i *cluster.Node) *Node { + addrs := utils.SliceToMapArrayO(addrOfNode[i.UUID], func(i *cluster.NodeAddr) (string, string) { + return i.Type, i.Addr + }) + + return &Node{ + Uuid: i.UUID, + Name: i.Name, + Cluster: i.Cluster, + Peer: addrs["peer"], + Admin: addrs["admin"], + Server: addrs["server"], + CreateTime: i.UpdateTime, + } + }), nil + +} + +func (s *imlClusterService) CountByPartition(ctx context.Context) (map[string]int, error) { + return s.store.Count(ctx) +} +func (s *imlClusterService) Search(ctx context.Context, keyword string, clusterId ...string) ([]*Cluster, error) { + wheres := make([]string, 0, 2) + value := make([]interface{}, 0, 3) + if keyword != "" { + wheres = append(wheres, "(`name` like ? or `resume` like ? or `uuid` like ?)") + value = append(value, "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%") + } + + if len(clusterId) > 0 { + if len(clusterId) == 1 { + wheres = append(wheres, "`uuid` = ?") + value = append(value, clusterId[0]) + } else { + wheres = append(wheres, "`uuid` in (?)") + value = append(value, clusterId) + } + + } + if len(wheres) == 0 { + return s.List(ctx) + } + where := strings.Join(wheres, " and ") + list, err := s.store.ListQuery(ctx, where, value, "update_at desc") + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromEntity), nil +} +func (s *imlClusterService) List(ctx context.Context, clusterIds ...string) ([]*Cluster, error) { + if len(clusterIds) == 0 { + list, err := s.store.List(ctx, make(map[string]interface{})) + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromEntity), nil + } + list, err := s.store.ListQuery(ctx, "`cluster` in (?)", []interface{}{clusterIds}, "update_at desc") + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromEntity), nil + +} + +func init() { + autowire.Auto[IClusterService](func() reflect.Value { + return reflect.ValueOf(&imlClusterService{}) + }) +} diff --git a/service/cluster/model.go b/service/cluster/model.go new file mode 100644 index 00000000..d6857f87 --- /dev/null +++ b/service/cluster/model.go @@ -0,0 +1,40 @@ +package cluster + +import ( + "time" + + "github.com/APIParkLab/APIPark/stores/cluster" +) + +type Cluster struct { + Uuid string + Name string + Resume string + Creator string + Updater string + Status int + CreateTime time.Time + UpdateTime time.Time +} + +func FromEntity(entity *cluster.Cluster) *Cluster { + return &Cluster{ + Uuid: entity.UUID, + Name: entity.Name, + Resume: entity.Resume, + Creator: entity.Creator, + Updater: entity.Updater, + CreateTime: entity.CreateAt, + UpdateTime: entity.UpdateAt, + } +} + +type Node struct { + Uuid string + Name string + Cluster string + Peer []string + Admin []string + Server []string + CreateTime time.Time +} diff --git a/service/dynamic-module/iml.go b/service/dynamic-module/iml.go new file mode 100644 index 00000000..84931203 --- /dev/null +++ b/service/dynamic-module/iml.go @@ -0,0 +1,135 @@ +package dynamic_module + +import ( + "context" + "errors" + "time" + + "github.com/eolinker/go-common/utils" + + "gorm.io/gorm" + + dynamic_module "github.com/APIParkLab/APIPark/stores/dynamic-module" + + "github.com/APIParkLab/APIPark/service/universally" +) + +var _ IDynamicModuleService = (*imlDynamicModuleService)(nil) + +type imlDynamicModuleService struct { + store dynamic_module.IDynamicModuleStore `autowired:""` + universally.IServiceGet[DynamicModule] + universally.IServiceDelete + universally.IServiceCreate[CreateDynamicModule] + universally.IServiceEdit[EditDynamicModule] +} + +func (i *imlDynamicModuleService) ListByPartition(ctx context.Context, partitionId string) ([]*DynamicModule, error) { + list, err := i.store.List(ctx, map[string]interface{}{"partition": partitionId}) + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromEntity), nil + +} + +func (i *imlDynamicModuleService) OnComplete() { + i.IServiceGet = universally.NewGet[DynamicModule, dynamic_module.DynamicModule](i.store, FromEntity) + + i.IServiceDelete = universally.NewDelete[dynamic_module.DynamicModule](i.store) + + i.IServiceCreate = universally.NewCreator[CreateDynamicModule, dynamic_module.DynamicModule](i.store, "dynamic_module", createEntityHandler, uniquestHandler, labelHandler) + + i.IServiceEdit = universally.NewEdit[EditDynamicModule, dynamic_module.DynamicModule](i.store, updateHandler, labelHandler) +} + +func labelHandler(e *dynamic_module.DynamicModule) []string { + return []string{e.Name, e.UUID, e.Description} +} +func uniquestHandler(i *CreateDynamicModule) []map[string]interface{} { + return []map[string]interface{}{{"uuid": i.Id}} +} +func createEntityHandler(i *CreateDynamicModule) *dynamic_module.DynamicModule { + now := time.Now() + return &dynamic_module.DynamicModule{ + UUID: i.Id, + Name: i.Name, + Driver: i.Driver, + Module: i.Module, + Version: i.Version, + Description: i.Description, + Config: i.Config, + Profession: i.Profession, + Skill: i.Skill, + CreateAt: now, + UpdateAt: now, + } +} + +func updateHandler(e *dynamic_module.DynamicModule, i *EditDynamicModule) { + if i.Name != nil { + e.Name = *i.Name + } + if i.Description != nil { + e.Description = *i.Description + } + if i.Config != nil { + e.Config = *i.Config + } + if i.Version != nil { + e.Version = *i.Version + } + e.UpdateAt = time.Now() +} + +var _ IDynamicModulePublishService = &imlDynamicModulePublishService{} + +type imlDynamicModulePublishService struct { + store dynamic_module.IDynamicModulePublishStore `autowired:""` + universally.IServiceCreate[CreateDynamicModulePublish] +} + +func (i *imlDynamicModulePublishService) OnComplete() { + i.IServiceCreate = universally.NewCreator[CreateDynamicModulePublish, dynamic_module.DynamicModulePublish](i.store, "dynamic_module_publish", i.createEntityHandler, i.uniquestHandler, i.labelHandler) +} + +func (i *imlDynamicModulePublishService) Latest(ctx context.Context, dmID string, clusters []string) (map[string]*DynamicModulePublish, error) { + result := make(map[string]*DynamicModulePublish) + for _, c := range clusters { + info, err := i.store.First(ctx, map[string]interface{}{"dynamic_module": dmID, "cluster": c}, "create_at desc") + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + continue + } + return nil, err + } + result[c] = &DynamicModulePublish{ + ID: info.UUID, + DynamicModule: info.DynamicModule, + Module: info.Module, + Cluster: info.Cluster, + Creator: info.Creator, + Version: info.Version, + CreateAt: info.CreateAt, + } + } + return result, nil +} + +func (i *imlDynamicModulePublishService) labelHandler(e *dynamic_module.DynamicModulePublish) []string { + return []string{e.UUID} +} +func (i *imlDynamicModulePublishService) uniquestHandler(m *CreateDynamicModulePublish) []map[string]interface{} { + return []map[string]interface{}{{"uuid": m.ID}} +} +func (i *imlDynamicModulePublishService) createEntityHandler(m *CreateDynamicModulePublish) *dynamic_module.DynamicModulePublish { + now := time.Now() + return &dynamic_module.DynamicModulePublish{ + UUID: m.ID, + DynamicModule: m.DynamicModule, + Module: m.Module, + Cluster: m.Cluster, + Version: m.Version, + CreateAt: now, + } +} diff --git a/service/dynamic-module/model.go b/service/dynamic-module/model.go new file mode 100644 index 00000000..cc35735c --- /dev/null +++ b/service/dynamic-module/model.go @@ -0,0 +1,92 @@ +package dynamic_module + +import ( + "time" + + dynamic_module "github.com/APIParkLab/APIPark/stores/dynamic-module" +) + +type DynamicModule struct { + ID string + Name string + Partition string + Driver string + Description string + Version string + Config string + Module string + Profession string + Skill string + Creator string + Updater string + CreateAt time.Time + UpdateAt time.Time +} + +type CreateDynamicModule struct { + Id string + Name string + Driver string + //Cluster string + Description string + Config string + Module string + Profession string + Skill string + Version string +} + +type EditDynamicModule struct { + Name *string + Description *string + Config *string + Version *string +} + +func FromEntity(ov *dynamic_module.DynamicModule) *DynamicModule { + return &DynamicModule{ + ID: ov.UUID, + Name: ov.Name, + Driver: ov.Driver, + Description: ov.Description, + Version: ov.Version, + Config: ov.Config, + Module: ov.Module, + Profession: ov.Profession, + Skill: ov.Skill, + Creator: ov.Creator, + Updater: ov.Updater, + CreateAt: ov.CreateAt, + UpdateAt: ov.UpdateAt, + } +} + +type DynamicModulePublish struct { + ID string + DynamicModule string + Module string + Cluster string + Creator string + Version string + CreateAt time.Time +} + +func FromPublishEntity(ov *dynamic_module.DynamicModulePublish) *DynamicModulePublish { + return &DynamicModulePublish{ + ID: ov.UUID, + DynamicModule: ov.DynamicModule, + Module: ov.Module, + Cluster: ov.Cluster, + Version: ov.Version, + Creator: ov.Creator, + CreateAt: ov.CreateAt, + } +} + +type CreateDynamicModulePublish struct { + ID string + DynamicModule string + Module string + Cluster string + Version string +} diff --git a/service/dynamic-module/service.go b/service/dynamic-module/service.go new file mode 100644 index 00000000..0e721791 --- /dev/null +++ b/service/dynamic-module/service.go @@ -0,0 +1,31 @@ +package dynamic_module + +import ( + "context" + "reflect" + + "github.com/APIParkLab/APIPark/service/universally" + "github.com/eolinker/go-common/autowire" +) + +type IDynamicModuleService interface { + universally.IServiceGet[DynamicModule] + universally.IServiceDelete + universally.IServiceCreate[CreateDynamicModule] + universally.IServiceEdit[EditDynamicModule] + ListByPartition(ctx context.Context, partitionId string) ([]*DynamicModule, error) +} + +type IDynamicModulePublishService interface { + universally.IServiceCreate[CreateDynamicModulePublish] + Latest(ctx context.Context, dmID string, partitionIds []string) (map[string]*DynamicModulePublish, error) +} + +func init() { + autowire.Auto[IDynamicModuleService](func() reflect.Value { + return reflect.ValueOf(new(imlDynamicModuleService)) + }) + autowire.Auto[IDynamicModulePublishService](func() reflect.Value { + return reflect.ValueOf(new(imlDynamicModulePublishService)) + }) +} diff --git a/service/permit-type/driver.go b/service/permit-type/driver.go new file mode 100644 index 00000000..43694b4a --- /dev/null +++ b/service/permit-type/driver.go @@ -0,0 +1,79 @@ +package permit_type + +import ( + "context" + "strings" + + "github.com/eolinker/go-common/auto" + "github.com/eolinker/go-common/utils" +) + +type Target struct { + Key string `json:"key"` + Type PermitType `json:"type"` + Name string `json:"name"` + Label string `json:"label"` + Tag string `json:"tag"` +} + +func TargetsOf(ks ...string) []*Target { + vs := make([]*Target, 0, len(ks)) + for _, k := range ks { + vs = append(vs, TargetOf(k)) + } + return vs +} +func TargetOf(key string) *Target { + index := strings.Index(key, ":") + if index > 0 { + + tp := Parse(key[0:index]) + if tp != Invalid { + name := key[index+1:] + + if tp == Special { + if v, has := specialRoles[name]; has { + return v + } + } + return tp.Target(name, "unknown") + } + } + return &Target{ + Type: Invalid, + Name: key, + Label: key, + } + +} + +func CompleteLabels(ctx context.Context, vs ...*Target) { + ml := turn(vs) + for n, vm := range ml { + if n == Special { + continue + } else if service, has := auto.GetService(n.Name()); has { + nl := utils.MapKeys(vm) + labels := service.GetLabels(ctx, nl...) + if labels == nil { + continue + } + for _, v := range vs { + if lv, h := labels[v.Name]; h { + v.Label = lv + } + } + } + } +} + +func turn(vs []*Target) map[PermitType]map[string]*Target { + rs := make(map[PermitType]map[string]*Target) + for _, v := range vs { + if _, has := rs[v.Type]; !has { + rs[v.Type] = make(map[string]*Target) + } + rs[v.Type][v.Name] = v + } + return rs +} diff --git a/service/permit-type/special.go b/service/permit-type/special.go new file mode 100644 index 00000000..f16298c7 --- /dev/null +++ b/service/permit-type/special.go @@ -0,0 +1,21 @@ +package permit_type + +var ( + AnyOne = Special.Target("any", "所有人") + All = Special.Target("all", "所有人") + TeamMember = Special.Target("team_member", "团队成员") + TeamMaster = Special.Target("team_master", "团队负责人") + ProjectMember = Special.Target("project_member", "系统成员") + ProjectMaster = Special.Target("project_master", "系统负责人") +) + +var ( + specialRoles = map[string]*Target{ + All.Name: All, + AnyOne.Name: AnyOne, + TeamMember.Name: TeamMember, + TeamMaster.Name: TeamMaster, + ProjectMember.Name: ProjectMember, + ProjectMaster.Name: ProjectMaster, + } +) diff --git a/service/permit-type/type.go b/service/permit-type/type.go new file mode 100644 index 00000000..480675ac --- /dev/null +++ b/service/permit-type/type.go @@ -0,0 +1,68 @@ +package permit_type + +import ( + "encoding/json" + "fmt" + "strings" +) + +type PermitType int + +func (p PermitType) MarshalJSON() ([]byte, error) { + return json.Marshal(p.Name()) +} + +func (p PermitType) Target(name string, label string) *Target { + return &Target{ + Key: p.KeyOf(name), + Type: p, + Name: name, + Label: label, + Tag: p.Tag(), + } +} + +const ( + Invalid PermitType = iota + Role + // Special 专属 + Special + // UserGroup 用户组 + UserGroup + // User 用户 + User + maxLength +) + +var ( + names = []string{"invalid", "role", "special", "user_group", "user"} + tags = []string{"invalid", "角色", "特殊角色", "用户组", "用户"} + + indexMap = make(map[string]PermitType) +) + +func init() { + if len(names) != int(maxLength) { + panic("init permit error") + } + for i := Invalid; i < maxLength; i++ { + indexMap[names[i]] = i + } + +} +func (p PermitType) KeyOf(v string) string { + return fmt.Sprint(p.Name(), ":", v) +} +func (p PermitType) Name() string { + return names[p] +} +func (p PermitType) Tag() string { + return tags[p] +} +func Parse(v string) PermitType { + p, has := indexMap[strings.ToLower(v)] + if has { + return p + } + return Invalid +} diff --git a/service/plugin-cluster/iml.go b/service/plugin-cluster/iml.go new file mode 100644 index 00000000..c7cf4ace --- /dev/null +++ b/service/plugin-cluster/iml.go @@ -0,0 +1,254 @@ +package plugin_cluster + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/APIParkLab/APIPark/model/plugin_model" + "github.com/APIParkLab/APIPark/stores/plugin" + "github.com/eolinker/go-common/utils" + "gorm.io/gorm" +) + +var ( + _ IPluginService = (*imlPluginService)(nil) +) + +type imlPluginService struct { + defineStore plugin.IPluginDefineStore `autowired:""` + pluginPartitionStore plugin.IPartitionPluginStore `autowired:""` +} + +func (i *imlPluginService) SaveDefine(ctx context.Context, defines []*plugin_model.Define) error { + return i.defineStore.Transaction(ctx, func(txCtx context.Context) error { + ov, err := i.defineStore.List(ctx, map[string]interface{}{}) + if err != nil { + return err + } + + ovm := utils.SliceToMap(ov, func(v *plugin.Define) string { + return v.Name + }) + + vsInsert := make([]*plugin.Define, 0, len(defines)) + vsUpdate := make([]*plugin.Define, 0, len(defines)) + for sort, dv := range defines { + ev := &plugin.Define{ + Id: 0, + Extend: dv.Extend, + Name: dv.Name, + Cname: dv.Cname, + Description: dv.Desc, + Kind: dv.Kind, + Status: dv.Status, + Render: dv.Render, + Config: dv.Config, + Sort: sort, + UpdateTime: time.Now(), + } + if oev, ok := ovm[dv.Name]; ok { + ev.Id = oev.Id + + delete(ovm, dv.Name) + vsUpdate = append(vsUpdate, ev) + } else { + vsInsert = append(vsInsert, ev) + } + } + // 删除多余的 + _, err = i.defineStore.Delete(ctx, utils.MapToSlice(ovm, func(n string, v *plugin.Define) int64 { + return v.Id + })...) + if err != nil { + return err + } + if len(vsInsert) > 0 { + err = i.defineStore.Insert(ctx, vsInsert...) + if err != nil { + return err + } + } + + for _, v := range vsUpdate { + _, err := i.defineStore.Update(ctx, v) + if err != nil { + return err + } + } + + return nil + }) +} + +func (i *imlPluginService) GetDefine(ctx context.Context, name string) (*PluginDefine, error) { + define, err := i.defineStore.First(ctx, map[string]interface{}{ + "name": name, + }) + if err != nil { + return nil, err + } + return FromEntity(define), nil + +} + +func (i *imlPluginService) GetConfig(ctx context.Context, clusterId string, name string) (*Config, *PluginDefine, error) { + + define, err := i.defineStore.First(ctx, map[string]interface{}{ + "name": name, + }) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil, fmt.Errorf("plugin define not found: %s", name) + } + return nil, nil, err + } + conf, _ := i.pluginPartitionStore.First(ctx, map[string]interface{}{ + "partition": clusterId, + "Plugin": name, + }) + if conf == nil { + return &Config{ + Plugin: name, + Status: define.Status, + Config: define.Config, + Operator: "", + }, FromEntity(define), nil + + } + return ConfigFromStore(conf), FromEntity(define), nil + +} + +func (i *imlPluginService) Defines(ctx context.Context, kind ...plugin_model.Kind) ([]*PluginDefine, error) { + if len(kind) == 0 { + list, err := i.defineStore.List(ctx, map[string]interface{}{}, "sort asc") + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromEntity), nil + + } else { + list, err := i.defineStore.List(ctx, map[string]interface{}{ + "kind": kind[0], + }, "sort asc") + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromEntity), nil + } +} + +func (i *imlPluginService) Options(ctx context.Context) []*PluginOption { + list, err := i.defineStore.List(ctx, map[string]interface{}{ + "kind": plugin_model.OpenKind, + }) + if err != nil { + return nil + } + return utils.SliceToSlice(list, func(s *plugin.Define) *PluginOption { + return &PluginOption{ + Cname: s.Cname, + Desc: s.Description, + Name: s.Name, + } + }) +} + +func (i *imlPluginService) SetCluster(ctx context.Context, clusterId string, name string, status plugin_model.Status, config plugin_model.ConfigType) error { + operator := utils.UserId(ctx) + + return i.defineStore.Transaction(ctx, func(txCtx context.Context) error { + + define, err := i.defineStore.First(ctx, map[string]interface{}{ + "name": name, + }) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("plugin not exits:%s", name) + } + return err + } + if define.Kind != plugin_model.OpenKind { + return fmt.Errorf("plugin not support config: %s[%s] width %s", define.Cname, name, define.Cname) + } + conf, err := i.pluginPartitionStore.First(ctx, map[string]interface{}{ + "partition": clusterId, + "name": name, + }) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if conf == nil { + conf = &plugin.Partition{ + Id: 0, + Partition: clusterId, + Plugin: define.Name, + Config: config, + Status: status, + CreateTime: time.Now(), + UpdateTime: time.Now(), + Operator: operator, + } + return i.pluginPartitionStore.Insert(ctx, conf) + } else { + conf.Config = config + conf.Status = status + conf.Operator = operator + conf.UpdateTime = time.Now() + _, err := i.pluginPartitionStore.Update(ctx, conf) + if err != nil { + return err + } + return nil + } + + }) + +} + +func (i *imlPluginService) ListCluster(ctx context.Context, clusterId string, kind ...plugin_model.Kind) ([]*ConfigPartition, error) { + + defines, err := i.Defines(ctx, kind...) + if err != nil { + return nil, err + } + + configList, err := i.pluginPartitionStore.List(ctx, map[string]interface{}{ + "partition": clusterId, + }) + if err != nil { + return nil, err + } + configMap := utils.SliceToMap(configList, func(t *plugin.Partition) string { + return t.Plugin + }) + + return utils.SliceToSlice(defines, func(d *PluginDefine) *ConfigPartition { + c, has := configMap[d.Name] + if has { + return &ConfigPartition{ + Extend: d.Extend, + Cname: d.Cname, + Desc: d.Name, + Config: ConfigFromStore(c), + } + } else { + return &ConfigPartition{ + Extend: d.Extend, + Cname: d.Cname, + Desc: d.Name, + + Config: &Config{ + Plugin: d.Name, + Status: d.Status, + Config: d.Config, + + Operator: "", + }, + } + } + }), nil + +} diff --git a/service/plugin-cluster/model.go b/service/plugin-cluster/model.go new file mode 100644 index 00000000..4a581ceb --- /dev/null +++ b/service/plugin-cluster/model.go @@ -0,0 +1,66 @@ +package plugin_cluster + +import ( + "time" + + "github.com/APIParkLab/APIPark/model/plugin_model" + "github.com/APIParkLab/APIPark/stores/plugin" +) + +type PluginOption struct { + Name string + Cname string + Desc string +} +type PluginDefine struct { + Extend string + Name string + Cname string + Desc string + Kind plugin_model.Kind + Status plugin_model.Status + Config plugin_model.ConfigType + Render plugin_model.Render + Update time.Time +} + +func FromEntity(s *plugin.Define) *PluginDefine { + return &PluginDefine{ + Extend: s.Extend, + Name: s.Name, + Cname: s.Cname, + Desc: s.Description, + Kind: s.Kind, + Status: s.Status, + Config: s.Config, + Render: s.Render, + Update: s.UpdateTime, + } +} + +type ConfigPartition struct { + *Config + Extend string + Cname string + Desc string +} + +type Config struct { + Plugin string + Status plugin_model.Status + Config plugin_model.ConfigType + Update *time.Time + Create *time.Time + Operator string +} + +func ConfigFromStore(partition *plugin.Partition) *Config { + return &Config{ + Plugin: partition.Plugin, + Status: partition.Status, + Config: partition.Config, + Update: &partition.UpdateTime, + Create: &partition.CreateTime, + Operator: partition.Operator, + } +} diff --git a/service/plugin-cluster/service.go b/service/plugin-cluster/service.go new file mode 100644 index 00000000..795c38f5 --- /dev/null +++ b/service/plugin-cluster/service.go @@ -0,0 +1,26 @@ +package plugin_cluster + +import ( + "context" + "reflect" + + "github.com/APIParkLab/APIPark/model/plugin_model" + "github.com/eolinker/go-common/autowire" +) + +type IPluginService interface { + Defines(ctx context.Context, kind ...plugin_model.Kind) ([]*PluginDefine, error) + Options(ctx context.Context) []*PluginOption + SetCluster(ctx context.Context, clusterId string, name string, status plugin_model.Status, config plugin_model.ConfigType) error + ListCluster(ctx context.Context, clusterId string, kind ...plugin_model.Kind) ([]*ConfigPartition, error) + GetConfig(ctx context.Context, clusterId string, name string) (*Config, *PluginDefine, error) + GetDefine(ctx context.Context, name string) (*PluginDefine, error) + SaveDefine(ctx context.Context, defines []*plugin_model.Define) error +} + +func init() { + autowire.Auto[IPluginService](func() reflect.Value { + return reflect.ValueOf(new(imlPluginService)) + }) + +} diff --git a/service/publish/diff.go b/service/publish/diff.go new file mode 100644 index 00000000..acec34e6 --- /dev/null +++ b/service/publish/diff.go @@ -0,0 +1,5 @@ +package publish + +import "github.com/APIParkLab/APIPark/service/service_diff" + +type Diff = service_diff.Diff diff --git a/service/publish/iml.go b/service/publish/iml.go new file mode 100644 index 00000000..7d21544f --- /dev/null +++ b/service/publish/iml.go @@ -0,0 +1,296 @@ +package publish + +import ( + "context" + "encoding/json" + "errors" + "strings" + "time" + + "github.com/APIParkLab/APIPark/service/service_diff" + "github.com/APIParkLab/APIPark/stores/publish" + "github.com/eolinker/go-common/utils" + "gorm.io/gorm" +) + +var ( + _ IPublishService = (*imlPublishService)(nil) +) + +type imlPublishService struct { + store publish.IPublishStore `autowired:""` + latestStore publish.IPublishLatestStore `autowired:""` + diffStore publish.IDiffStore `autowired:""` + statusStore publish.IPublishStatusStore `autowired:""` +} + +func (s *imlPublishService) SetPublishStatus(ctx context.Context, status *Status) error { + return s.statusStore.Save(ctx, &publish.Status{ + Publish: status.Publish, + Cluster: status.Cluster, + Status: int(status.Status), + Error: status.Error, + UpdateAt: time.Now(), + }) +} + +func (s *imlPublishService) GetPublishStatus(ctx context.Context, id string) ([]*Status, error) { + list, err := s.statusStore.List(ctx, map[string]interface{}{"publish": id}) + if err != nil { + return nil, err + } + + return utils.SliceToSlice(list, func(s *publish.Status) *Status { + return &Status{ + Publish: s.Publish, + Cluster: s.Cluster, + Status: StatusType(s.Status), + Error: s.Error, + UpdateAt: s.UpdateAt, + } + }), nil +} + +func (s *imlPublishService) setAction(ctx context.Context, service, id string, status StatusType, comments string) error { + operator := utils.UserId(ctx) + return s.store.Transaction(ctx, func(ctx context.Context) error { + ev, err := s.store.GetByUUID(ctx, id) + if err != nil { + return err + } + if ev.Service != service { + return errors.New("service not match") + } + ev.Status = int(status) + ev.Comments = comments + ev.Approver = operator + ev.ApproveTime = time.Now() + _, err = s.store.Update(ctx, ev) + return err + }) +} +func (s *imlPublishService) Refuse(ctx context.Context, service, id string, comments string) error { + return s.setAction(ctx, service, id, StatusRefuse, comments) +} + +func (s *imlPublishService) Accept(ctx context.Context, service, id string, comments string) error { + return s.setAction(ctx, service, id, StatusAccept, comments) + +} + +func (s *imlPublishService) SetLatest(ctx context.Context, release string, id string) error { + if id == "" { + return s.RemoveLatest(ctx, release) + } + e := &publish.Latest{ + Id: 0, + Release: release, + Latest: id, + } + err := s.latestStore.Save(ctx, e) + if err != nil { + return err + } + return nil +} + +func (s *imlPublishService) RemoveLatest(ctx context.Context, release string) error { + //_, err := s.latestStore.DeleteQuery(ctx, "release = ?", release) + _, err := s.latestStore.DeleteWhere(ctx, map[string]interface{}{"release": release}) + return err +} + +func (s *imlPublishService) Latest(ctx context.Context, release ...string) ([]*Publish, error) { + if len(release) == 0 { + return nil, nil + } + var latestList []*publish.Latest + var err error + if len(release) == 1 { + latestList, err = s.latestStore.ListQuery(ctx, "`release` = ?", []interface{}{release[0]}, "id asc") + } else { + latestList, err = s.latestStore.ListQuery(ctx, "`release` in (?)", []interface{}{release}, "id asc") + } + if err != nil { + return nil, err + } + if len(latestList) == 0 { + return nil, nil + } + var flows []*publish.Publish + if len(latestList) == 1 { + f, err := s.store.GetByUUID(ctx, latestList[0].Latest) + if err != nil { + return nil, err + } + flows = []*publish.Publish{f} + } else { + flowsIds := utils.SliceToSlice(latestList, func(v *publish.Latest) string { + return v.Latest + }) + flows, err = s.store.ListQuery(ctx, "uuid in (?)", []interface{}{flowsIds}, "id asc") + if err != nil { + return nil, err + } + } + + return utils.SliceToSlice(flows, FromEntity), nil + +} + +func (s *imlPublishService) GetLatest(ctx context.Context, id string) (*Publish, error) { + latest, err := s.Latest(ctx, id) + if err != nil { + return nil, err + } + if len(latest) == 0 { + return nil, gorm.ErrRecordNotFound + } + return latest[0], nil +} + +func (s *imlPublishService) Get(ctx context.Context, id string) (*Publish, error) { + env, err := s.store.GetByUUID(ctx, id) + if err != nil { + return nil, err + } + return FromEntity(env), nil +} + +func (s *imlPublishService) GetDiff(ctx context.Context, id string) (*service_diff.Diff, error) { + ev, err := s.diffStore.GetByUUID(ctx, id) + if err != nil { + return nil, err + } + df := new(service_diff.Diff) + err = json.Unmarshal(ev.Data, df) + if err != nil { + return nil, err + } + return df, nil +} + +func (s *imlPublishService) ListProject(ctx context.Context, service string) ([]*Publish, error) { + flows, err := s.store.ListQuery(ctx, "service = ?", []interface{}{service}, "apply_time desc") + if err != nil { + return nil, err + } + return utils.SliceToSlice(flows, FromEntity), nil +} + +func (s *imlPublishService) ListProjectPage(ctx context.Context, service string, page int, pageSize int) ([]*Publish, int64, error) { + flows, total, err := s.store.ListPage(ctx, "service = ?", page, pageSize, []interface{}{service}, "apply_time desc") + if err != nil { + return nil, 0, err + } + return utils.SliceToSlice(flows, FromEntity), total, nil +} + +func (s *imlPublishService) ListForStatus(ctx context.Context, service string, status ...StatusType) ([]*Publish, error) { + + wheres := make([]string, 0, 2) + args := make([]interface{}, 0, 2) + if service != "" { + wheres = append(wheres, "service = ?") + args = append(args, service) + } + if len(status) == 1 { + wheres = append(wheres, "status = ?") + args = append(args, status[0]) + } else if len(status) > 1 { + wheres = append(wheres, "status in (?)") + args = append(args, status) + } + if len(wheres) == 0 { + return nil, errors.New("required status") + } + + flows, err := s.store.ListQuery(ctx, strings.Join(wheres, " and "), args, "id asc") + if err != nil { + return nil, err + } + return utils.SliceToSlice(flows, FromEntity), nil +} +func (s *imlPublishService) ListForStatusPage(ctx context.Context, page int, pageSize int, status ...StatusType) ([]*Publish, int64, error) { + + if len(status) == 0 { + return nil, 0, errors.New("required status") + } + if len(status) == 1 { + flows, total, err := s.store.ListPage(ctx, "status = ?", page, pageSize, []interface{}{status[0]}, "id asc") + if err != nil { + return nil, 0, err + } + return utils.SliceToSlice(flows, FromEntity), total, nil + } + flows, total, err := s.store.ListPage(ctx, "status in (?)", page, pageSize, []interface{}{status}, "id asc") + if err != nil { + return nil, 0, err + } + return utils.SliceToSlice(flows, FromEntity), total, nil +} + +func (s *imlPublishService) Create(ctx context.Context, uuid, service, release, previous, version, remark string, df *service_diff.Diff) error { + operator := utils.UserId(ctx) + data, err := json.Marshal(df) + if err != nil { + return err + } + err = s.store.Transaction(ctx, func(ctx context.Context) error { + nv := &publish.Publish{ + Id: 0, + UUID: uuid, + Service: service, + Release: release, + Previous: previous, + Version: version, + ApplyTime: time.Now(), + Applicant: operator, + Remark: remark, + ApproveTime: time.Time{}, + Approver: "", + Comments: "", + Status: int(StatusApply), + } + err := s.store.Insert(ctx, nv) + if err != nil { + return err + } + + // diff + err = s.diffStore.Insert(ctx, &publish.Diff{ + Id: nv.Id, + UUID: uuid, + Data: data}) + if err != nil { + return err + } + return s.latestStore.Save(ctx, &publish.Latest{ + Id: 0, + Release: release, + Latest: uuid, + }) + }) + if err != nil { + return err + } + return nil +} + +func (s *imlPublishService) SetStatus(ctx context.Context, service, id string, status StatusType) error { + return s.store.Transaction(ctx, func(ctx context.Context) error { + ev, err := s.store.GetByUUID(ctx, id) + if err != nil { + return err + } + if ev.Service != service { + return errors.New("service not match") + } + ev.Status = int(status) + _, err = s.store.Update(ctx, ev) + if err != nil { + return err + } + return err + }) +} diff --git a/service/publish/model.go b/service/publish/model.go new file mode 100644 index 00000000..31ebeeef --- /dev/null +++ b/service/publish/model.go @@ -0,0 +1,47 @@ +package publish + +import ( + "time" + + "github.com/APIParkLab/APIPark/stores/publish" +) + +type Publish struct { + Id string + Service string + Release string + Previous string + Version string + ApplyTime time.Time + Applicant string + Remark string + ApproveTime time.Time + Approver string + Comments string + Status StatusType +} + +func FromEntity(e *publish.Publish) *Publish { + return &Publish{ + Id: e.UUID, + Service: e.Service, + Release: e.Release, + Previous: e.Previous, + Version: e.Version, + ApplyTime: e.ApplyTime, + Applicant: e.Applicant, + Remark: e.Remark, + ApproveTime: e.ApproveTime, + Approver: e.Approver, + Comments: e.Comments, + Status: StatusType(e.Status), + } +} + +type Status struct { + Publish string + Cluster string + Status StatusType + Error string + UpdateAt time.Time +} diff --git a/service/publish/service.go b/service/publish/service.go new file mode 100644 index 00000000..75549523 --- /dev/null +++ b/service/publish/service.go @@ -0,0 +1,37 @@ +package publish + +import ( + "context" + "reflect" + + "github.com/APIParkLab/APIPark/service/service_diff" + "github.com/eolinker/go-common/autowire" +) + +type IPublishService interface { + SetLatest(ctx context.Context, release string, id string) error + RemoveLatest(ctx context.Context, release string) error + Latest(ctx context.Context, release ...string) ([]*Publish, error) + GetLatest(ctx context.Context, id string) (*Publish, error) + Get(ctx context.Context, id string) (*Publish, error) + GetDiff(ctx context.Context, id string) (*service_diff.Diff, error) + ListProject(ctx context.Context, project string) ([]*Publish, error) + ListProjectPage(ctx context.Context, project string, page int, pageSize int) ([]*Publish, int64, error) + ListForStatus(ctx context.Context, project string, status ...StatusType) ([]*Publish, error) + ListForStatusPage(ctx context.Context, page int, pageSize int, status ...StatusType) ([]*Publish, int64, error) + + Create(ctx context.Context, uuid, project, release, previous, version, remark string, diff *service_diff.Diff) error + + SetStatus(ctx context.Context, project, id string, status StatusType) error + Refuse(ctx context.Context, project, id string, comments string) error + Accept(ctx context.Context, project, id string, comments string) error + + SetPublishStatus(ctx context.Context, status *Status) error + GetPublishStatus(ctx context.Context, id string) ([]*Status, error) +} + +func init() { + autowire.Auto[IPublishService](func() reflect.Value { + return reflect.ValueOf(new(imlPublishService)) + }) +} diff --git a/service/publish/type.go b/service/publish/type.go new file mode 100644 index 00000000..64f08e3b --- /dev/null +++ b/service/publish/type.go @@ -0,0 +1,29 @@ +package publish + +import "encoding/json" + +type StatusType int + +const ( + StatusNone StatusType = iota + StatusApply //审批中 + StatusAccept // 审批通过 + StatusRefuse // 审批拒绝 + StatusDone // 已发布 + StatusStop // 已中止 + StatusClose // 已关闭 + StatusPublishing // 发布中 + StatusPublishError // 发布失败 +) + +var ( + names = []string{"none", "apply", "accept", "refuse", "done", "stop", "close", "publishing", "error"} +) + +func (s StatusType) String() string { + return names[s] +} + +func (s StatusType) MarshalJSON() ([]byte, error) { + return json.Marshal(s.String()) +} diff --git a/service/release/commit.go b/service/release/commit.go new file mode 100644 index 00000000..5e61526d --- /dev/null +++ b/service/release/commit.go @@ -0,0 +1,9 @@ +package release + +type CommitType = string + +const ( + CommitApiDocument CommitType = "api_doc" + CommitUpstream CommitType = "upstream" + CommitApiProxy CommitType = "api_proxy" +) diff --git a/service/release/iml.go b/service/release/iml.go new file mode 100644 index 00000000..46bc19a0 --- /dev/null +++ b/service/release/iml.go @@ -0,0 +1,460 @@ +package release + +import ( + "context" + "errors" + "time" + + "github.com/APIParkLab/APIPark/service/api" + "github.com/APIParkLab/APIPark/service/universally/commit" + "github.com/APIParkLab/APIPark/service/upstream" + "github.com/APIParkLab/APIPark/stores/release" + "github.com/eolinker/go-common/auto" + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/utils" + "github.com/google/uuid" + "gorm.io/gorm" +) + +var ( + _ IReleaseService = (*imlReleaseService)(nil) + _ auto.CompleteService = (*imlReleaseService)(nil) + _ autowire.Complete = (*imlReleaseService)(nil) +) + +type imlReleaseService struct { + releaseStore release.IReleaseStore `autowired:""` + commitStore release.IReleaseCommitStore `autowired:""` + releaseRuntime release.IReleaseRuntime `autowired:""` +} + +func (s *imlReleaseService) Completeness(partitions []string, apis []string, proxyCommits []*commit.Commit[api.Proxy], documentCommits []*commit.Commit[api.Document], upstreamCommits []*commit.Commit[upstream.Config]) bool { + + proxys := utils.SliceToMap(proxyCommits, func(o *commit.Commit[api.Proxy]) string { + return o.Target + }) + + documents := utils.SliceToMap(documentCommits, func(o *commit.Commit[api.Document]) string { + return o.Target + }) + for _, aid := range apis { + _, has := proxys[aid] + if !has { + return false + } + + _, has = documents[aid] + if !has { + return false + } + + } + upstreamMap := make(map[string]map[string]struct{}) + for _, upstreamCommit := range upstreamCommits { + if _, has := upstreamMap[upstreamCommit.Target]; !has { + upstreamMap[upstreamCommit.Target] = make(map[string]struct{}) + } + upstreamMap[upstreamCommit.Target][upstreamCommit.Key] = struct{}{} + } + + for _, partition := range partitions { + for _, u := range upstreamMap { + if _, has := u[partition]; !has { + return false + } + } + } + + return true +} + +func (s *imlReleaseService) GetCommits(ctx context.Context, id string) ([]*ProjectCommits, error) { + list, err := s.commitStore.List(ctx, map[string]interface{}{ + "release": id, + }) + if err != nil { + return nil, err + } + + return utils.SliceToSlice(list, func(o *release.Commit) *ProjectCommits { + return &ProjectCommits{ + Release: o.Release, + Target: o.Target, + Key: o.Key, + Type: o.Type, + Commit: o.Commit, + } + }), nil +} + +func (s *imlReleaseService) OnComplete() { + auto.RegisterService("release", s) +} + +func (s *imlReleaseService) GetLabels(ctx context.Context, ids ...string) map[string]string { + if len(ids) == 0 { + return nil + } + if len(ids) == 1 { + o, err := s.releaseStore.GetByUUID(ctx, ids[0]) + if err != nil || o == nil { + return nil + } + return map[string]string{ + o.UUID: o.Name, + } + } + list, err := s.releaseStore.ListQuery(ctx, "`uuid` in ?", []interface{}{ids}, "id") + if err != nil { + return nil + } + return utils.SliceToMapO(list, func(o *release.Release) (string, string) { return o.UUID, o.Name }) +} + +func (s *imlReleaseService) GetApiProxyCommit(ctx context.Context, id string, apiUUID string) (string, error) { + commits, err := s.getCommitByType(ctx, id, CommitApiProxy, apiUUID, CommitApiProxy) + if err != nil { + return "", err + } + if len(commits) == 0 { + return "", errors.New("not found") + } + + return commits[0].Commit, nil +} + +func (s *imlReleaseService) getCommitByType(ctx context.Context, releaseId, t CommitType, target string, key string) ([]*release.Commit, error) { + where := "`release` = ? and `type` = ? and `target` = ?" + args := []interface{}{releaseId, t, target} + if len(key) > 0 { + if len(key) == 1 { + where += " and `key` = ?" + args = append(args, key[0]) + } else { + where += " and `key` in ?" + args = append(args, key) + } + } + return s.commitStore.ListQuery(ctx, where, args, "") + +} +func (s *imlReleaseService) GetApiDocCommit(ctx context.Context, id string, apiUUID string) (string, error) { + commits, err := s.getCommitByType(ctx, id, CommitApiDocument, apiUUID, CommitApiDocument) + if err != nil { + return "", err + } + if len(commits) == 0 { + return "", errors.New("not found") + } + + return commits[0].Commit, nil +} + +func (s *imlReleaseService) GetRunningApiDocCommit(ctx context.Context, service string, apiUUID string) (string, error) { + running, err := s.releaseRuntime.First(ctx, map[string]interface{}{ + "service": service, + }) + if err != nil { + return "", err + } + return s.GetApiDocCommit(ctx, running.Release, apiUUID) + +} + +func (s *imlReleaseService) GetRunningApiProxyCommit(ctx context.Context, service string, apiUUID string) (string, error) { + running, err := s.releaseRuntime.First(ctx, map[string]interface{}{ + "service": service, + }) + if err != nil { + return "", err + } + return s.GetApiProxyCommit(ctx, running.Release, apiUUID) +} + +// +//func (s *imlReleaseService) DiffApis(ctx context.Context, baseApis []*Api, targetApis []*Api) []*APiDiff { +// result := make([]*APiDiff, 0, len(targetApis)+len(baseApis)) +// baseApiMap := utils.SliceToMap(baseApis, func(v *Api) string { +// return v.Api +// }) +// for _, targetApi := range targetApis { +// if baseApi, ok := baseApiMap[targetApi.Api]; ok { +// if baseApi.ProxyCommit != targetApi.ProxyCommit || baseApi.DocCommit != targetApi.DocCommit { +// result = append(result, &APiDiff{ +// Api: targetApi.Api, +// Change: project_diff.ChangeTypeUpdate, +// }) +// } else { +// result = append(result, &APiDiff{ +// Api: targetApi.Api, +// Change: project_diff.ChangeTypeNone, +// }) +// } +// delete(baseApiMap, targetApi.Api) +// } else { +// result = append(result, &APiDiff{ +// Api: targetApi.Api, +// Change: project_diff.ChangeTypeNew, +// }) +// } +// } +// for _, baseApi := range baseApiMap { +// result = append(result, &APiDiff{ +// Api: baseApi.Api, +// Change: project_diff.ChangeTypeDelete, +// }) +// } +// return result +//} +// +//func (s *imlReleaseService) DiffUpstreams(ctx context.Context, baseUpstreams []*UpstreamCommit, targetUpstreams []*UpstreamCommit) []*UpstreamDiff { +// Upstreams := make([]*UpstreamDiff, 0, len(targetUpstreams)+len(baseUpstreams)) +// baseUpstreamMap := utils.SliceToMap(baseUpstreams, func(v *UpstreamCommit) string { +// return fmt.Sprintf("%s-%s", v.UpstreamCommit, v.Cluster) +// }) +// for _, targetUpstream := range targetUpstreams { +// key := fmt.Sprintf("%s-%s", targetUpstream.UpstreamCommit, targetUpstream.Cluster) +// if baseUpstream, ok := baseUpstreamMap[key]; ok { +// if baseUpstream.Commit != targetUpstream.Commit { +// Upstreams = append(Upstreams, &UpstreamDiff{ +// UpstreamCommit: targetUpstream.UpstreamCommit, +// Cluster: targetUpstream.Cluster, +// Commit: targetUpstream.Commit, +// Change: project_diff.ChangeTypeUpdate, +// }) +// } else { +// Upstreams = append(Upstreams, &UpstreamDiff{ +// UpstreamCommit: targetUpstream.UpstreamCommit, +// Cluster: targetUpstream.Cluster, +// Commit: targetUpstream.Commit, +// Change: project_diff.ChangeTypeNone, +// }) +// } +// delete(baseUpstreamMap, targetUpstream.UpstreamCommit) +// } else { +// Upstreams = append(Upstreams, &UpstreamDiff{ +// UpstreamCommit: targetUpstream.UpstreamCommit, +// Cluster: targetUpstream.Cluster, +// Commit: targetUpstream.Commit, +// Change: project_diff.ChangeTypeNew, +// }) +// } +// } +// for _, baseUpstream := range baseUpstreamMap { +// Upstreams = append(Upstreams, &UpstreamDiff{ +// UpstreamCommit: baseUpstream.UpstreamCommit, +// Cluster: baseUpstream.Cluster, +// Commit: baseUpstream.Commit, +// Change: project_diff.ChangeTypeDelete, +// }) +// } +// return Upstreams +//} + +func (s *imlReleaseService) SetRunning(ctx context.Context, service string, id string) error { + _, err := s.releaseRuntime.DeleteWhere(ctx, map[string]interface{}{"service": service}) + if err != nil { + return err + } + operator := utils.UserId(ctx) + return s.releaseRuntime.Save(ctx, &release.Runtime{ + Id: 0, + Service: service, + Release: id, + UpdateTime: time.Now(), + Operator: operator, + }) + +} + +func (s *imlReleaseService) CreateRelease(ctx context.Context, service string, version string, remark string, apisProxyCommits, apiDocCommits map[string]string, upstreams map[string]map[string]string) (*Release, error) { + operator := utils.UserId(ctx) + releaseId := uuid.NewString() + commits := make([]*release.Commit, 0, len(apisProxyCommits)+len(apiDocCommits)+len(upstreams)) + for aid, commitUUID := range apisProxyCommits { + commits = append(commits, &release.Commit{ + Type: CommitApiProxy, + Target: aid, + Release: releaseId, + Key: CommitApiProxy, + Commit: commitUUID, + }) + } + for apiId, commitUUID := range apiDocCommits { + commits = append(commits, &release.Commit{ + Type: CommitApiDocument, + Target: apiId, + Release: releaseId, + Key: CommitApiDocument, + Commit: commitUUID, + }) + } + for upId, upstreamsByPartition := range upstreams { + for partition, commitUUID := range upstreamsByPartition { + commits = append(commits, &release.Commit{ + Type: CommitUpstream, + Target: upId, + Release: releaseId, + Key: partition, + Commit: commitUUID, + }) + } + } + ev := &release.Release{ + Id: 0, + UUID: releaseId, + Name: version, + Service: service, + Remark: remark, + Creator: operator, + CreateAt: time.Now(), + } + err := s.releaseStore.Transaction(ctx, func(ctx context.Context) error { + ok, e := s.CheckNewVersion(ctx, service, version) + if e != nil { + return e + } + if !ok { + return errors.New("version already exists") + } + + err := s.releaseStore.Insert(ctx, ev) + if err != nil { + return err + } + return s.commitStore.Insert(ctx, commits...) + }) + if err != nil { + return nil, err + } + return FromEntity(ev), nil +} + +func (s *imlReleaseService) CheckNewVersion(ctx context.Context, service string, version string) (bool, error) { + v, err := s.releaseStore.First(ctx, map[string]interface{}{ + "service": service, + "name": version, + }) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return false, err + } + if errors.Is(err, gorm.ErrRecordNotFound) || v == nil { + return true, nil + } + return false, nil +} + +func (s *imlReleaseService) GetRelease(ctx context.Context, id string) (*Release, error) { + r, err := s.releaseStore.GetByUUID(ctx, id) + if err != nil { + return nil, err + } + return FromEntity(r), nil +} + +// +//func (s *imlReleaseService) Diff(ctx context.Context, baseReleaseId string, targetReleaseId string) (*Diff, error) { +// if baseReleaseId != "" || targetReleaseId != "" { +// return nil, errors.New("not support") +// } +// baseApis, baseUpstreams, err := s.GetReleaseInfos(ctx, baseReleaseId) +// if err != nil { +// return nil, err +// } +// targetApis, targetUpstreams, err := s.GetReleaseInfos(ctx, targetReleaseId) +// if err != nil { +// return nil, err +// } +// +// df := new(Diff) +// df.Apis = s.DiffApis(ctx, baseApis, targetApis) +// df.Upstreams = s.DiffUpstreams(ctx, baseUpstreams, targetUpstreams) +// return df, nil +//} + +func (s *imlReleaseService) DeleteRelease(ctx context.Context, id string) error { + //todo 判断版本是否有使用中的未完结流程 + + return s.releaseStore.Transaction(ctx, func(ctx context.Context) error { + first, err := s.releaseRuntime.First(ctx, map[string]interface{}{ + "release": id, + }) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if err == nil && first != nil { + return errors.New("release is in use") + } + err = s.releaseStore.DeleteUUID(ctx, id) + if err != nil { + return err + } + _, err = s.commitStore.DeleteWhere(ctx, map[string]interface{}{ + "release": id, + }) + if err != nil { + return err + } + return nil + }) + +} + +func (s *imlReleaseService) List(ctx context.Context, service string) ([]*Release, error) { + list, err := s.releaseStore.List(ctx, map[string]interface{}{"service": service}, "create_at desc") + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromEntity), nil +} + +func (s *imlReleaseService) GetReleaseInfos(ctx context.Context, id string) ([]*APIProxyCommit, []*APIDocumentCommit, []*UpstreamCommit, error) { + commits, err := s.commitStore.List(ctx, map[string]interface{}{ + "release": id, + }) + if err != nil { + return nil, nil, nil, err + } + apiProxyCommits := make([]*APIProxyCommit, 0, len(commits)) + apiDocumentCommits := make([]*APIDocumentCommit, 0, len(commits)) + upstreamCommits := make([]*UpstreamCommit, 0, len(commits)) + + for _, v := range commits { + switch v.Type { + case CommitApiProxy: + apiProxyCommits = append(apiProxyCommits, &APIProxyCommit{ + Release: v.Release, + API: v.Target, + Commit: v.Commit, + }) + + case CommitApiDocument: + apiDocumentCommits = append(apiDocumentCommits, &APIDocumentCommit{ + Release: v.Release, + API: v.Target, + Commit: v.Commit, + }) + + case CommitUpstream: + upstreamCommits = append(upstreamCommits, &UpstreamCommit{ + Release: v.Release, + Upstream: v.Target, + Partition: v.Key, + Commit: v.Commit, + }) + } + + } + + return apiProxyCommits, apiDocumentCommits, upstreamCommits, nil +} + +func (s *imlReleaseService) GetRunning(ctx context.Context, service string) (*Release, error) { + running, err := s.releaseRuntime.First(ctx, map[string]interface{}{ + "service": service, + }) + if err != nil { + return nil, err + } + return s.GetRelease(ctx, running.Release) +} diff --git a/service/release/model.go b/service/release/model.go new file mode 100644 index 00000000..68e75c04 --- /dev/null +++ b/service/release/model.go @@ -0,0 +1,70 @@ +package release + +import ( + "time" + + "github.com/APIParkLab/APIPark/stores/release" +) + +type Release struct { + UUID string + Service string + Version string + Remark string + Creator string + CreateAt time.Time +} + +func FromEntity(e *release.Release) *Release { + return &Release{ + UUID: e.UUID, + Service: e.Service, + Version: e.Name, + Remark: e.Remark, + Creator: e.Creator, + CreateAt: e.CreateAt, + } +} + +type APIProxyCommit struct { + Release string + API string + Commit string +} +type APIDocumentCommit struct { + Release string + API string + Commit string +} +type UpstreamCommit struct { + Release string + Upstream string + Partition string + Commit string +} + +type ProjectCommits struct { + Release string + Type string + Target string + Key string + Commit string +} + +//type Diff struct { +// Apis []*APiDiff `json:"apis"` +// Upstreams []*UpstreamDiff `json:"upstream"` +//} + +//type APiDiff struct { +// Api string `json:"api,omitempty"` +// +// Change project_diff.ChangeType `json:"change,omitempty"` +//} +// +//type UpstreamDiff struct { +// UpstreamCommit string `json:"upstream,omitempty"` +// Cluster string `json:"partition,omitempty"` +// Commit string `json:"commit,omitempty"` +// Change project_diff.ChangeType `json:"change,omitempty"` +//} diff --git a/service/release/service.go b/service/release/service.go new file mode 100644 index 00000000..3dd254e3 --- /dev/null +++ b/service/release/service.go @@ -0,0 +1,45 @@ +package release + +import ( + "context" + "reflect" + + "github.com/APIParkLab/APIPark/service/api" + "github.com/APIParkLab/APIPark/service/universally/commit" + "github.com/APIParkLab/APIPark/service/upstream" + "github.com/eolinker/go-common/autowire" +) + +type IReleaseService interface { + // GetRelease 获取发布信息 + GetRelease(ctx context.Context, id string) (*Release, error) + // CreateRelease 创建发布 + CreateRelease(ctx context.Context, service string, version string, remark string, apisProxyCommits, apiDocCommits map[string]string, upstreams map[string]map[string]string) (*Release, error) + // DeleteRelease 删除发布 + DeleteRelease(ctx context.Context, id string) error + List(ctx context.Context, service string) ([]*Release, error) + GetApiProxyCommit(ctx context.Context, id string, apiUUID string) (string, error) + GetApiDocCommit(ctx context.Context, id string, apiUUID string) (string, error) + GetReleaseInfos(ctx context.Context, id string) ([]*APIProxyCommit, []*APIDocumentCommit, []*UpstreamCommit, error) + GetCommits(ctx context.Context, id string) ([]*ProjectCommits, error) + + GetRunningApiDocCommit(ctx context.Context, service string, apiUUID string) (string, error) + GetRunningApiProxyCommit(ctx context.Context, service string, apiUUID string) (string, error) + Completeness(partitions []string, apis []string, proxyCommits []*commit.Commit[api.Proxy], documentCommits []*commit.Commit[api.Document], upstreamCommits []*commit.Commit[upstream.Config]) bool + + // GetRunning gets the running release with the given service. + // + // ctx: the context + // service: the service name + // Return type(s): *Release, error + GetRunning(ctx context.Context, service string) (*Release, error) + + SetRunning(ctx context.Context, service string, id string) error + CheckNewVersion(ctx context.Context, service string, version string) (bool, error) +} + +func init() { + autowire.Auto[IReleaseService](func() reflect.Value { + return reflect.ValueOf(new(imlReleaseService)) + }) +} diff --git a/service/serivce.go b/service/serivce.go new file mode 100644 index 00000000..58fc4137 --- /dev/null +++ b/service/serivce.go @@ -0,0 +1,5 @@ +// Description: This package is used to define the service interface +// 只能使用 store 下面的封装好的接口 +// 只能使用对应的store,不允许跨模块的store + +package service diff --git a/service/service-doc/iml.go b/service/service-doc/iml.go new file mode 100644 index 00000000..aedc1502 --- /dev/null +++ b/service/service-doc/iml.go @@ -0,0 +1,60 @@ +package service_doc + +import ( + "context" + "errors" + "time" + + "github.com/eolinker/go-common/utils" + "gorm.io/gorm" + + "github.com/APIParkLab/APIPark/stores/service" +) + +type imlDocService struct { + store service.IServiceDocStore `autowired:""` +} + +func (i *imlDocService) Save(ctx context.Context, input *SaveDoc) error { + info, err := i.Get(ctx, input.Sid) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + userID := utils.UserId(ctx) + if info != nil { + _, err = i.store.Update(ctx, &service.Doc{ + Id: info.ID, + Sid: input.Sid, + Doc: input.Doc, + CreateAt: info.CreateTime, + UpdateAt: time.Now(), + Creator: info.Creator, + Updater: userID, + }) + return err + } + return i.store.Insert(ctx, &service.Doc{ + Sid: input.Sid, + Doc: input.Doc, + CreateAt: time.Now(), + UpdateAt: time.Now(), + Creator: userID, + Updater: userID, + }) +} + +func (i *imlDocService) Get(ctx context.Context, sid string) (*Doc, error) { + doc, err := i.store.First(ctx, map[string]interface{}{"sid": sid}) + if err != nil { + return nil, err + } + return &Doc{ + ID: doc.Id, + DocID: doc.Sid, + Creator: doc.Creator, + Updater: doc.Updater, + Doc: doc.Doc, + UpdateTime: doc.UpdateAt, + CreateTime: doc.CreateAt, + }, nil +} diff --git a/service/service-doc/model.go b/service/service-doc/model.go new file mode 100644 index 00000000..ef0dd89e --- /dev/null +++ b/service/service-doc/model.go @@ -0,0 +1,19 @@ +package service_doc + +import "time" + +type Doc struct { + ID int64 + DocID string + Name string + Creator string + Updater string + Doc string + UpdateTime time.Time + CreateTime time.Time +} + +type SaveDoc struct { + Sid string + Doc string +} diff --git a/service/service-doc/service.go b/service/service-doc/service.go new file mode 100644 index 00000000..451182d9 --- /dev/null +++ b/service/service-doc/service.go @@ -0,0 +1,19 @@ +package service_doc + +import ( + "context" + "reflect" + + "github.com/eolinker/go-common/autowire" +) + +type IDocService interface { + Get(ctx context.Context, sid string) (*Doc, error) + Save(ctx context.Context, input *SaveDoc) error +} + +func init() { + autowire.Auto[IDocService](func() reflect.Value { + return reflect.ValueOf(new(imlDocService)) + }) +} diff --git a/service/service-tag/iml.go b/service/service-tag/iml.go new file mode 100644 index 00000000..7607c678 --- /dev/null +++ b/service/service-tag/iml.go @@ -0,0 +1,54 @@ +package service_tag + +import ( + "context" + + "github.com/APIParkLab/APIPark/stores/service" + "github.com/eolinker/go-common/utils" +) + +type imlTagService struct { + store service.IServiceTagStore `autowired:""` +} + +func (i *imlTagService) List(ctx context.Context, sids []string, tids []string) ([]*Tag, error) { + condition := make(map[string]interface{}) + if len(sids) > 0 { + condition["sid"] = sids + } + if len(tids) > 0 { + condition["tid"] = tids + } + result, err := i.store.List(ctx, condition) + if err != nil { + return nil, err + } + return utils.SliceToSlice(result, func(s *service.Tag) *Tag { + return &Tag{ + Tid: s.Tid, + Sid: s.Sid, + } + }), nil +} + +func (i *imlTagService) Delete(ctx context.Context, tids []string, sids []string) error { + if len(tids) == 0 && len(sids) == 0 { + return nil + } + conditions := make(map[string]interface{}) + if len(tids) > 0 { + conditions["tid"] = tids + } + if len(sids) > 0 { + conditions["sid"] = sids + } + _, err := i.store.DeleteWhere(ctx, conditions) + return err +} + +func (i *imlTagService) Create(ctx context.Context, input *CreateTag) error { + return i.store.Insert(ctx, &service.Tag{ + Sid: input.Sid, + Tid: input.Tid, + }) +} diff --git a/service/service-tag/model.go b/service/service-tag/model.go new file mode 100644 index 00000000..44a639bc --- /dev/null +++ b/service/service-tag/model.go @@ -0,0 +1,11 @@ +package service_tag + +type CreateTag struct { + Tid string + Sid string +} + +type Tag struct { + Tid string + Sid string +} diff --git a/service/service-tag/service.go b/service/service-tag/service.go new file mode 100644 index 00000000..b09afc37 --- /dev/null +++ b/service/service-tag/service.go @@ -0,0 +1,20 @@ +package service_tag + +import ( + "context" + "reflect" + + "github.com/eolinker/go-common/autowire" +) + +type ITagService interface { + Delete(ctx context.Context, tids []string, sids []string) error + Create(ctx context.Context, input *CreateTag) error + List(ctx context.Context, sids []string, tids []string) ([]*Tag, error) +} + +func init() { + autowire.Auto[ITagService](func() reflect.Value { + return reflect.ValueOf(new(imlTagService)) + }) +} diff --git a/service/service/iml.go b/service/service/iml.go new file mode 100644 index 00000000..f0a79ea4 --- /dev/null +++ b/service/service/iml.go @@ -0,0 +1,155 @@ +package service + +import ( + "context" + "fmt" + "time" + + "github.com/eolinker/go-common/utils" + + "github.com/eolinker/go-common/auto" + + "github.com/APIParkLab/APIPark/service/universally" + "github.com/APIParkLab/APIPark/stores/service" +) + +var ( + _ IServiceService = (*imlServiceService)(nil) +) + +type imlServiceService struct { + store service.IServiceStore `autowired:""` + universally.IServiceGet[Service] + universally.IServiceDelete + universally.IServiceCreate[Create] + universally.IServiceEdit[Edit] +} + +func (i *imlServiceService) SearchPublicServices(ctx context.Context, keyword string) ([]*Service, error) { + w := map[string]interface{}{ + "as_server": true, + "service_type": PublicService.Int(), + } + list, err := i.store.Search(ctx, keyword, w) + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromEntity), nil +} + +func (i *imlServiceService) ServiceCountByTeam(ctx context.Context, teamId ...string) (map[string]int64, error) { + w := map[string]interface{}{ + "as_server": true, + } + if len(teamId) > 0 { + w["team"] = teamId + } + return i.store.CountByGroup(ctx, "", w, "team") +} + +func (i *imlServiceService) AppCountByTeam(ctx context.Context, teamId ...string) (map[string]int64, error) { + w := map[string]interface{}{ + "as_app": true, + } + if len(teamId) > 0 { + w["team"] = teamId + } + return i.store.CountByGroup(ctx, "", w, "team") +} + +func (i *imlServiceService) AppList(ctx context.Context, appIds ...string) ([]*Service, error) { + w := make(map[string]interface{}) + if len(appIds) > 0 { + w["uuid"] = appIds + } + w["as_app"] = true + list, err := i.store.List(ctx, w) + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromEntity), nil +} + +func (i *imlServiceService) Check(ctx context.Context, id string, rule map[string]bool) (*Service, error) { + pro, err := i.Get(ctx, id) + if err != nil { + return nil, err + } + if rule == nil || len(rule) == 0 { + return pro, nil + } + if rule["as_server"] && !pro.AsServer { + return nil, fmt.Errorf("project %s is not as server", id) + } + if rule["as_app"] && !pro.AsApp { + return nil, fmt.Errorf("project %s is not as app", id) + } + return pro, nil +} + +func (i *imlServiceService) GetLabels(ctx context.Context, ids ...string) map[string]string { + if len(ids) == 0 { + return nil + } + list, err := i.store.ListQuery(ctx, "`uuid` in (?)", []interface{}{ids}, "id") + if err != nil { + return nil + } + return utils.SliceToMapO(list, func(i *service.Service) (string, string) { + return i.UUID, i.Name + }) +} + +func (i *imlServiceService) OnComplete() { + i.IServiceGet = universally.NewGetSoftDelete[Service, service.Service](i.store, FromEntity) + + i.IServiceDelete = universally.NewSoftDelete[service.Service](i.store) + + i.IServiceCreate = universally.NewCreatorSoftDelete[Create, service.Service](i.store, "service", createEntityHandler, uniquestHandler, labelHandler) + + i.IServiceEdit = universally.NewEdit[Edit, service.Service](i.store, updateHandler, labelHandler) + auto.RegisterService("service", i) +} + +func labelHandler(e *service.Service) []string { + return []string{e.Name, e.UUID, e.Description} +} +func uniquestHandler(i *Create) []map[string]interface{} { + return []map[string]interface{}{{"uuid": i.Id}} +} +func createEntityHandler(i *Create) *service.Service { + now := time.Now() + return &service.Service{ + Id: 0, + UUID: i.Id, + Name: i.Name, + CreateAt: now, + UpdateAt: now, + Description: i.Description, + Logo: i.Logo, + Prefix: i.Prefix, + Team: i.Team, + ServiceType: i.ServiceType.Int(), + Catalogue: i.Catalogue, + AsServer: i.AsServer, + AsApp: i.AsApp, + } +} +func updateHandler(e *service.Service, i *Edit) { + if i.Name != nil { + e.Name = *i.Name + } + if i.Description != nil { + e.Description = *i.Description + } + if i.ServiceType != nil { + e.ServiceType = (*i.ServiceType).Int() + } + if i.Catalogue != nil { + e.Catalogue = *i.Catalogue + } + if i.Logo != nil { + e.Logo = *i.Logo + } + +} diff --git a/service/service/model.go b/service/service/model.go new file mode 100644 index 00000000..988b54ed --- /dev/null +++ b/service/service/model.go @@ -0,0 +1,100 @@ +package service + +import ( + "time" + + "github.com/APIParkLab/APIPark/stores/service" +) + +const ( + InnerService ServiceType = "inner" + PublicService ServiceType = "public" + UnknownService ServiceType = "unknown" +) + +type ServiceType string + +func (s ServiceType) String() string { + return string(s) +} + +func (s ServiceType) Int() int { + switch s { + case InnerService: + return 1 + case PublicService: + return 2 + default: + return 0 + } +} + +func ToServiceType(s int) ServiceType { + + switch s { + case 1: + return InnerService + case 2: + return PublicService + default: + return UnknownService + } +} + +type Service struct { + Id string + Name string + Description string + Team string + Prefix string + Logo string + ServiceType ServiceType + Catalogue string + AsServer bool + AsApp bool + CreateTime time.Time + UpdateTime time.Time +} + +func FromEntity(e *service.Service) *Service { + return &Service{ + Id: e.UUID, + Name: e.Name, + Description: e.Description, + Team: e.Team, + Prefix: e.Prefix, + Logo: e.Logo, + ServiceType: ToServiceType(e.ServiceType), + Catalogue: e.Catalogue, + AsServer: e.AsServer, + AsApp: e.AsApp, + CreateTime: e.CreateAt, + UpdateTime: e.UpdateAt, + } +} + +type Create struct { + Id string + Name string + Description string + Team string + Prefix string + Logo string + ServiceType ServiceType + Catalogue string + AsServer bool + AsApp bool +} + +type Edit struct { + Name *string + Description *string + ServiceType *ServiceType + Catalogue *string + Logo *string +} + +type CreateTag struct { + Tid string + Sid string +} diff --git a/service/service/service.go b/service/service/service.go new file mode 100644 index 00000000..2373ba12 --- /dev/null +++ b/service/service/service.go @@ -0,0 +1,27 @@ +package service + +import ( + "context" + "reflect" + + "github.com/APIParkLab/APIPark/service/universally" + "github.com/eolinker/go-common/autowire" +) + +type IServiceService interface { + universally.IServiceGet[Service] + universally.IServiceDelete + universally.IServiceCreate[Create] + universally.IServiceEdit[Edit] + ServiceCountByTeam(ctx context.Context, teamId ...string) (map[string]int64, error) + AppCountByTeam(ctx context.Context, teamId ...string) (map[string]int64, error) + SearchPublicServices(ctx context.Context, keyword string) ([]*Service, error) + Check(ctx context.Context, id string, rule map[string]bool) (*Service, error) + AppList(ctx context.Context, appIds ...string) ([]*Service, error) +} + +func init() { + autowire.Auto[IServiceService](func() reflect.Value { + return reflect.ValueOf(new(imlServiceService)) + }) +} diff --git a/service/service_diff/diff.go b/service/service_diff/diff.go new file mode 100644 index 00000000..4fd12621 --- /dev/null +++ b/service/service_diff/diff.go @@ -0,0 +1,45 @@ +package service_diff + +import ( + "github.com/APIParkLab/APIPark/service/upstream" +) + +type StatusType int + +const ( + StatusOK StatusType = iota + StatusUnset // 未设置 + StatusLoss +) + +type Status struct { + Proxy StatusType `json:"proxy_status,omitempty"` + Doc StatusType `json:"doc_status,omitempty"` + //Upstream StatusType `json:"upstream_status,omitempty"` +} + +type ApiDiff struct { + APi string `json:"api,omitempty"` + Upstream string `json:"upstream,omitempty"` + Name string `json:"name,omitempty"` + Method string `json:"method,omitempty"` + Path string `json:"path,omitempty"` + Change ChangeType `json:"change,omitempty"` + Status Status `json:"status,omitempty"` +} +type UpstreamConfig struct { + Addr []string `json:"addr"` +} +type UpstreamDiff struct { + Upstream string `json:"upstream,omitempty" ` + //Partition string `json:"partition,omitempty"` + Data *upstream.Config `json:"data,omitempty"` + Change ChangeType `json:"change,omitempty"` + Status StatusType `json:"status,omitempty"` +} + +type Diff struct { + Clusters []string `json:"clusters,omitempty"` + Apis []*ApiDiff `json:"apis"` + Upstreams []*UpstreamDiff `json:"upstreams"` +} diff --git a/service/service_diff/type.go b/service/service_diff/type.go new file mode 100644 index 00000000..ef52a489 --- /dev/null +++ b/service/service_diff/type.go @@ -0,0 +1,67 @@ +package service_diff + +import ( + "encoding/json" + "strings" +) + +type ChangeType int + +func (c *ChangeType) UnmarshalJSON(bytes []byte) error { + var s string + err := json.Unmarshal(bytes, &s) + if err != nil { + return err + } + nv := ParseChangeType(s) + *c = nv + return nil +} + +func (c *ChangeType) MarshalJSON() ([]byte, error) { + return json.Marshal(c.String()) +} + +const ( + ChangeTypeNone ChangeType = iota + ChangeTypeNew + ChangeTypeUpdate + ChangeTypeDelete + maxChangeIndex +) + +var ( + changeTypeNames = []string{ + "none", + "new", + "update", + "delete", + } + changeTypeLabels = []string{ + "无", + "新增", + "更新", + "删除", + } + changeNameMaps = make(map[string]ChangeType) +) + +func init() { + for i, v := range changeTypeNames { + changeNameMaps[v] = ChangeType(i) + } +} +func (c *ChangeType) String() string { + return changeTypeNames[*c] +} + +func (c *ChangeType) Label() string { + return changeTypeLabels[*c] +} +func ParseChangeType(s string) ChangeType { + t, ok := changeNameMaps[strings.ToLower(s)] + if ok { + return t + } + return ChangeTypeNone +} diff --git a/service/setting/iml.go b/service/setting/iml.go new file mode 100644 index 00000000..82768c53 --- /dev/null +++ b/service/setting/iml.go @@ -0,0 +1,27 @@ +package setting + +import ( + "context" + "github.com/APIParkLab/APIPark/stores/setting" +) + +var ( + _ ISettingService = (*imlSettingService)(nil) +) + +type imlSettingService struct { + store setting.ISettingStore `autowired:""` +} + +func (i *imlSettingService) Get(ctx context.Context, name string) (string, bool) { + ev, err := i.store.Get(ctx, name) + if err != nil { + return "", false + } + return ev.Value, true +} + +func (i *imlSettingService) Set(ctx context.Context, name string, value string, operator string) error { + + return i.store.Set(ctx, name, value, operator) +} diff --git a/service/setting/setting.go b/service/setting/setting.go new file mode 100644 index 00000000..2a306766 --- /dev/null +++ b/service/setting/setting.go @@ -0,0 +1,19 @@ +package setting + +import ( + "context" + "github.com/eolinker/go-common/autowire" + "reflect" +) + +type ISettingService interface { + Get(ctx context.Context, name string) (value string, has bool) + Set(ctx context.Context, name string, value string, operator string) error + //All(ctx context.Context) map[string]string +} + +func init() { + autowire.Auto[ISettingService](func() reflect.Value { + return reflect.ValueOf(new(imlSettingService)) + }) +} diff --git a/service/subscribe/iml.go b/service/subscribe/iml.go new file mode 100644 index 00000000..1d6cd329 --- /dev/null +++ b/service/subscribe/iml.go @@ -0,0 +1,299 @@ +package subscribe + +import ( + "context" + "time" + + "github.com/eolinker/go-common/utils" + + "github.com/APIParkLab/APIPark/service/universally" + "github.com/APIParkLab/APIPark/stores/subscribe" +) + +const ( + // ApplyStatusRefuse 拒绝 + ApplyStatusRefuse = iota + // ApplyStatusReview 审核中 + ApplyStatusReview + // ApplyStatusSubscribe 已订阅 + ApplyStatusSubscribe + // ApplyStatusUnsubscribe 已退订 + ApplyStatusUnsubscribe + // ApplyStatusCancel 取消申请 + ApplyStatusCancel +) + +const ( + // FromUser 用户添加 + FromUser = iota + // FromSubscribe 订阅 + FromSubscribe +) + +var ( + _ ISubscribeService = (*imlSubscribeService)(nil) +) + +type imlSubscribeService struct { + store subscribe.ISubscribeStore `autowired:""` + universally.IServiceGet[Subscribe] + universally.IServiceDelete + universally.IServiceCreate[CreateSubscribe] + universally.IServiceEdit[UpdateSubscribe] +} + +func (i *imlSubscribeService) CountMapByService(ctx context.Context, status int, service ...string) (map[string]int64, error) { + w := make(map[string]interface{}) + if len(service) > 0 { + w["service"] = service + } + + w["apply_status"] = status + return i.store.CountByGroup(ctx, "", w, "service") +} + +func (i *imlSubscribeService) ListByServices(ctx context.Context, serviceIds ...string) ([]*Subscribe, error) { + w := make(map[string]interface{}) + if len(serviceIds) > 0 { + w["service"] = serviceIds + } + list, err := i.store.List(ctx, w, "create_at desc") + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromEntity), nil +} + +func (i *imlSubscribeService) SubscriptionsByApplication(ctx context.Context, applicationIds ...string) ([]*Subscribe, error) { + w := make(map[string]interface{}) + if len(applicationIds) > 0 { + w["application"] = applicationIds + } + + //w["apply_status"] = ApplyStatusSubscribe + list, err := i.store.List(ctx, w, "create_at desc") + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromEntity), nil +} + +func (i *imlSubscribeService) DeleteByApplication(ctx context.Context, service string, application string) error { + _, err := i.store.DeleteWhere(ctx, map[string]interface{}{"service": service, "application": application}) + return err +} + +func (i *imlSubscribeService) SubscribersByProject(ctx context.Context, serviceIds ...string) ([]*Subscribe, error) { + w := make(map[string]interface{}) + if len(serviceIds) > 0 { + w["service"] = serviceIds + } + + w["apply_status"] = ApplyStatusSubscribe + list, err := i.store.List(ctx, w, "create_at desc") + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromEntity), nil +} + +func (i *imlSubscribeService) Subscribers(ctx context.Context, service string, status int) ([]*Subscribe, error) { + list, err := i.store.List(ctx, map[string]interface{}{"apply_status": status, "service": service}, "create_at desc") + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromEntity), nil +} + +func (i *imlSubscribeService) ListBySubscribeStatus(ctx context.Context, serviceId string, status int) ([]*Subscribe, error) { + w := make(map[string]interface{}) + if serviceId != "" { + w["service"] = serviceId + } + w["apply_status"] = status + list, err := i.store.List(ctx, w, "create_at desc") + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromEntity), nil +} + +func (i *imlSubscribeService) UpdateSubscribeStatus(ctx context.Context, application string, service string, status int) error { + info, err := i.store.First(ctx, map[string]interface{}{"service": service, "application": application}) + if err != nil { + return err + } + info.ApplyStatus = status + info.ApproveAt = time.Now() + //info.Approver = utils.UserId(ctx) + return i.store.Save(ctx, info) +} + +func (i *imlSubscribeService) MySubscribeServices(ctx context.Context, application string, serviceIDs []string) ([]*Subscribe, error) { + w := make(map[string]interface{}) + + if len(serviceIDs) > 0 { + w["service"] = serviceIDs + } + //if len(partitionIds) > 0 { + // w["partition"] = partitionIds + //} + w["application"] = application + list, err := i.store.List(ctx, w, "create_at desc") + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromEntity), nil +} + +func (i *imlSubscribeService) ListByApplication(ctx context.Context, service string, application ...string) ([]*Subscribe, error) { + w := make(map[string]interface{}) + if len(application) > 0 { + w["application"] = application + } + w["service"] = service + list, err := i.store.List(ctx, w) + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromEntity), nil +} + +func (i *imlSubscribeService) OnComplete() { + i.IServiceGet = universally.NewGet[Subscribe, subscribe.Subscribe](i.store, FromEntity) + i.IServiceCreate = universally.NewCreator[CreateSubscribe, subscribe.Subscribe](i.store, "subscribe", i.createEntityHandler, i.uniquestHandler, i.labelHandler) + i.IServiceDelete = universally.NewDelete[subscribe.Subscribe](i.store) + i.IServiceEdit = universally.NewEdit[UpdateSubscribe, subscribe.Subscribe](i.store, i.updateHandler, i.labelHandler) +} + +func (i *imlSubscribeService) idHandler(e *subscribe.Subscribe) int64 { + return e.Id +} +func (i *imlSubscribeService) labelHandler(e *subscribe.Subscribe) []string { + return []string{e.Service} +} +func (i *imlSubscribeService) uniquestHandler(t *CreateSubscribe) []map[string]interface{} { + return []map[string]interface{}{{"uuid": t.Uuid}} +} +func (i *imlSubscribeService) createEntityHandler(t *CreateSubscribe) *subscribe.Subscribe { + return &subscribe.Subscribe{ + UUID: t.Uuid, + Application: t.Application, + Service: t.Service, + From: t.From, + Applier: t.Applier, + CreateAt: time.Now(), + ApplyStatus: t.ApplyStatus, + } +} + +func (i *imlSubscribeService) updateHandler(e *subscribe.Subscribe, t *UpdateSubscribe) { + //if t.Approver != nil { + // e.Approver = *t.Approver + //} + if t.ApplyStatus != nil { + e.ApplyStatus = *t.ApplyStatus + } + + //if t.ApplyID != nil { + // e.ApplyID = *t.ApplyID + //} +} + +var ( + _ ISubscribeApplyService = (*imlSubscribeApplyService)(nil) +) + +type imlSubscribeApplyService struct { + store subscribe.ISubscribeApplyStore `autowired:""` + universally.IServiceGet[Apply] + universally.IServiceDelete + universally.IServiceCreate[CreateApply] + universally.IServiceEdit[EditApply] +} + +func (i *imlSubscribeApplyService) GetApply(ctx context.Context, serviceId string, appId string) (*Apply, error) { + info, err := i.store.First(ctx, map[string]interface{}{"service": serviceId, "application": appId}) + if err != nil { + return nil, err + } + return FromApplyEntity(info), err +} + +func (i *imlSubscribeApplyService) ListByStatus(ctx context.Context, pid string, status ...int) ([]*Apply, error) { + w := make(map[string]interface{}) + w["service"] = pid + if len(status) > 0 { + w["status"] = status + } + list, err := i.store.List(ctx, w, "apply_at desc") + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, FromApplyEntity), nil +} + +func (i *imlSubscribeApplyService) RevokeById(ctx context.Context, id string) error { + _, err := i.store.UpdateWhere(ctx, map[string]interface{}{"uuid": id}, map[string]interface{}{"status": -1}) + return err +} + +func (i *imlSubscribeApplyService) Revoke(ctx context.Context, service string, application string) error { + _, err := i.store.UpdateWhere(ctx, map[string]interface{}{"service": service, "application": application}, map[string]interface{}{"status": -1}) + return err +} + +func (i *imlSubscribeApplyService) OnComplete() { + i.IServiceGet = universally.NewGet[Apply, subscribe.Apply](i.store, FromApplyEntity) + i.IServiceCreate = universally.NewCreator[CreateApply, subscribe.Apply](i.store, "subscribe_apply", i.createEntityHandler, i.uniquestHandler, i.labelHandler) + i.IServiceDelete = universally.NewDelete[subscribe.Apply](i.store) + i.IServiceEdit = universally.NewEdit[EditApply, subscribe.Apply](i.store, i.updateHandler, i.labelHandler) +} + +func (i *imlSubscribeApplyService) idHandler(e *subscribe.Apply) int64 { + return e.Id +} + +func (i *imlSubscribeApplyService) labelHandler(e *subscribe.Apply) []string { + return []string{e.Service} +} + +func (i *imlSubscribeApplyService) uniquestHandler(t *CreateApply) []map[string]interface{} { + return []map[string]interface{}{{"uuid": t.Uuid}} +} + +func (i *imlSubscribeApplyService) createEntityHandler(t *CreateApply) *subscribe.Apply { + now := time.Now() + return &subscribe.Apply{ + Uuid: t.Uuid, + Service: t.Service, + Team: t.Team, + Application: t.Application, + ApplyTeam: t.ApplyTeam, + Applier: t.Applier, + ApplyAt: now, + Approver: "", + ApproveAt: now, + Status: t.Status, + Opinion: "", + Reason: t.Reason, + } +} + +func (i *imlSubscribeApplyService) updateHandler(e *subscribe.Apply, t *EditApply) { + if t.Approver != nil { + e.Approver = *t.Approver + e.ApproveAt = time.Now() + } + if t.Status != nil { + e.Status = *t.Status + } + if t.Opinion != nil { + e.Opinion = *t.Opinion + } + if t.Applier != nil { + e.Applier = *t.Applier + e.ApplyAt = time.Now() + } + +} diff --git a/service/subscribe/model.go b/service/subscribe/model.go new file mode 100644 index 00000000..d9882145 --- /dev/null +++ b/service/subscribe/model.go @@ -0,0 +1,94 @@ +package subscribe + +import ( + "time" + + "github.com/APIParkLab/APIPark/stores/subscribe" +) + +type Subscribe struct { + Id string + Service string + + // 订阅方相关 + Application string + From int + Applier string + ApplyStatus int + CreateAt time.Time +} + +type CreateSubscribe struct { + Uuid string + Service string + Applier string + Application string + ApplyStatus int + From int +} + +type UpdateSubscribe struct { + ApplyStatus *int +} + +func FromEntity(e *subscribe.Subscribe) *Subscribe { + return &Subscribe{ + Id: e.UUID, + Service: e.Service, + ApplyStatus: e.ApplyStatus, + Application: e.Application, + Applier: e.Applier, + From: e.From, + CreateAt: e.CreateAt, + } +} + +type CreateApply struct { + Uuid string + Service string + Team string + Application string + ApplyTeam string + Reason string + Status int + Applier string +} + +type EditApply struct { + Opinion *string + Status *int + Applier *string + Approver *string +} + +type Apply struct { + Id string + Service string + Team string + Application string + ApplyTeam string + Applier string + ApplyAt time.Time + Approver string + ApproveAt time.Time + Status int + Opinion string + Reason string +} + +func FromApplyEntity(e *subscribe.Apply) *Apply { + return &Apply{ + Id: e.Uuid, + Service: e.Service, + Team: e.Team, + Application: e.Application, + ApplyTeam: e.ApplyTeam, + Applier: e.Applier, + ApplyAt: e.ApplyAt, + Approver: e.Approver, + ApproveAt: e.ApproveAt, + Status: e.Status, + Opinion: e.Opinion, + Reason: e.Reason, + } +} diff --git a/service/subscribe/service.go b/service/subscribe/service.go new file mode 100644 index 00000000..5aec3017 --- /dev/null +++ b/service/subscribe/service.go @@ -0,0 +1,48 @@ +package subscribe + +import ( + "context" + "reflect" + + "github.com/APIParkLab/APIPark/service/universally" + "github.com/eolinker/go-common/autowire" +) + +type ISubscribeService interface { + universally.IServiceGet[Subscribe] + universally.IServiceDelete + universally.IServiceCreate[CreateSubscribe] + universally.IServiceEdit[UpdateSubscribe] + CountMapByService(ctx context.Context, status int, service ...string) (map[string]int64, error) + DeleteByApplication(ctx context.Context, service string, application string) error + ListByApplication(ctx context.Context, service string, application ...string) ([]*Subscribe, error) + ListByServices(ctx context.Context, serviceIds ...string) ([]*Subscribe, error) + + MySubscribeServices(ctx context.Context, application string, serviceIDs []string) ([]*Subscribe, error) + UpdateSubscribeStatus(ctx context.Context, application string, service string, status int) error + ListBySubscribeStatus(ctx context.Context, projectId string, status int) ([]*Subscribe, error) + SubscribersByProject(ctx context.Context, projectIds ...string) ([]*Subscribe, error) + Subscribers(ctx context.Context, project string, status int) ([]*Subscribe, error) + SubscriptionsByApplication(ctx context.Context, applicationIds ...string) ([]*Subscribe, error) +} + +type ISubscribeApplyService interface { + universally.IServiceGet[Apply] + universally.IServiceDelete + universally.IServiceCreate[CreateApply] + universally.IServiceEdit[EditApply] + GetApply(ctx context.Context, serviceId string, appId string) (*Apply, error) + ListByStatus(ctx context.Context, pid string, status ...int) ([]*Apply, error) + Revoke(ctx context.Context, service string, application string) error + RevokeById(ctx context.Context, id string) error +} + +func init() { + autowire.Auto[ISubscribeService](func() reflect.Value { + return reflect.ValueOf(new(imlSubscribeService)) + }) + + autowire.Auto[ISubscribeApplyService](func() reflect.Value { + return reflect.ValueOf(new(imlSubscribeApplyService)) + }) +} diff --git a/service/tag/iml.go b/service/tag/iml.go new file mode 100644 index 00000000..dd489f57 --- /dev/null +++ b/service/tag/iml.go @@ -0,0 +1,59 @@ +package tag + +import ( + "context" + "time" + + "github.com/eolinker/go-common/auto" + "github.com/eolinker/go-common/utils" + + "github.com/APIParkLab/APIPark/stores/tag" + + "github.com/APIParkLab/APIPark/service/universally" +) + +var ( + _ ITagService = (*imlTagService)(nil) +) + +type imlTagService struct { + store tag.ITagStore `autowired:""` + universally.IServiceGet[Tag] + universally.IServiceDelete + universally.IServiceCreate[CreateTag] +} + +func (i *imlTagService) GetLabels(ctx context.Context, ids ...string) map[string]string { + if len(ids) == 0 { + return nil + } + list, err := i.store.ListQuery(ctx, "`uuid` in (?)", []interface{}{ids}, "id") + if err != nil { + return nil + } + return utils.SliceToMapO(list, func(i *tag.Tag) (string, string) { + return i.UUID, i.Name + }) +} + +func (i *imlTagService) OnComplete() { + i.IServiceGet = universally.NewGet[Tag, tag.Tag](i.store, FromEntity) + i.IServiceCreate = universally.NewCreator[CreateTag, tag.Tag](i.store, "catalogue", createEntityHandler, uniquestHandler, labelHandler) + i.IServiceDelete = universally.NewDelete[tag.Tag](i.store) + auto.RegisterService("tag", i) +} + +func labelHandler(e *tag.Tag) []string { + return []string{e.Name, e.UUID} +} +func uniquestHandler(i *CreateTag) []map[string]interface{} { + return []map[string]interface{}{{"uuid": i.Id}} +} +func createEntityHandler(i *CreateTag) *tag.Tag { + return &tag.Tag{ + UUID: i.Id, + Name: i.Name, + CreateAt: time.Now(), + UpdateAt: time.Now(), + } +} diff --git a/service/tag/model.go b/service/tag/model.go new file mode 100644 index 00000000..15320b7a --- /dev/null +++ b/service/tag/model.go @@ -0,0 +1,28 @@ +package tag + +import ( + "time" + + "github.com/APIParkLab/APIPark/stores/tag" +) + +type Tag struct { + Id string + Name string + CreateTime time.Time + UpdateTime time.Time +} + +func FromEntity(e *tag.Tag) *Tag { + return &Tag{ + Id: e.UUID, + Name: e.Name, + CreateTime: e.CreateAt, + UpdateTime: e.UpdateAt, + } +} + +type CreateTag struct { + Id string + Name string +} diff --git a/service/tag/service.go b/service/tag/service.go new file mode 100644 index 00000000..da184959 --- /dev/null +++ b/service/tag/service.go @@ -0,0 +1,20 @@ +package tag + +import ( + "reflect" + + "github.com/APIParkLab/APIPark/service/universally" + "github.com/eolinker/go-common/autowire" +) + +type ITagService interface { + universally.IServiceGet[Tag] + universally.IServiceDelete + universally.IServiceCreate[CreateTag] +} + +func init() { + autowire.Auto[ITagService](func() reflect.Value { + return reflect.ValueOf(new(imlTagService)) + }) +} diff --git a/service/team-member/model.go b/service/team-member/model.go new file mode 100644 index 00000000..d2e4ee36 --- /dev/null +++ b/service/team-member/model.go @@ -0,0 +1,7 @@ +package team_member + +import ( + "github.com/eolinker/ap-account/service/member" +) + +type Member = member.Member diff --git a/service/team-member/service.go b/service/team-member/service.go new file mode 100644 index 00000000..1782fa8d --- /dev/null +++ b/service/team-member/service.go @@ -0,0 +1,18 @@ +package team_member + +import ( + "reflect" + + "github.com/eolinker/go-common/autowire" + + "github.com/APIParkLab/APIPark/stores/team" + "github.com/eolinker/ap-account/service/member" +) + +type ITeamMemberService member.IMemberService + +func init() { + autowire.Auto[ITeamMemberService](func() reflect.Value { + return reflect.ValueOf(new(member.Service[team.ITeamMemberStore])) + }) +} diff --git a/service/team/iml.go b/service/team/iml.go new file mode 100644 index 00000000..4565b960 --- /dev/null +++ b/service/team/iml.go @@ -0,0 +1,75 @@ +package team + +import ( + "context" + "time" + + "github.com/eolinker/go-common/utils" + + "github.com/eolinker/go-common/auto" + + "github.com/APIParkLab/APIPark/service/universally" + "github.com/APIParkLab/APIPark/stores/team" +) + +var ( + _ ITeamService = (*imlTeamService)(nil) +) + +type imlTeamService struct { + teamStore team.ITeamStore `autowired:""` + universally.IServiceGet[Team] + universally.IServiceDelete + universally.IServiceCreate[CreateTeam] + universally.IServiceEdit[EditTeam] +} + +func (s *imlTeamService) OnComplete() { + s.IServiceGet = universally.NewGetSoftDelete[Team, team.Team](s.teamStore, FromEntity) + + s.IServiceDelete = universally.NewSoftDelete[team.Team](s.teamStore) + + s.IServiceCreate = universally.NewCreatorSoftDelete[CreateTeam, team.Team](s.teamStore, "team", createEntityHandler, uniquestHandler, labelHandler) + + s.IServiceEdit = universally.NewEdit[EditTeam, team.Team](s.teamStore, updateHandler, labelHandler) + auto.RegisterService("team", s) +} + +func (s *imlTeamService) GetLabels(ctx context.Context, ids ...string) map[string]string { + if len(ids) == 0 { + return nil + } + list, err := s.teamStore.ListQuery(ctx, "`uuid` in (?)", []interface{}{ids}, "id") + if err != nil { + return nil + } + return utils.SliceToMapO(list, func(i *team.Team) (string, string) { + return i.UUID, i.Name + }) +} +func labelHandler(e *team.Team) []string { + return []string{e.Name, e.UUID, e.Description} +} +func uniquestHandler(i *CreateTeam) []map[string]interface{} { + return []map[string]interface{}{{"uuid": i.Id}, {"name": i.Name}} +} +func createEntityHandler(i *CreateTeam) *team.Team { + return &team.Team{ + Id: 0, + UUID: i.Id, + Name: i.Name, + Description: i.Description, + CreateAt: time.Now(), + UpdateAt: time.Now(), + } +} +func updateHandler(e *team.Team, i *EditTeam) { + if i.Name != nil { + e.Name = *i.Name + } + if i.Description != nil { + e.Description = *i.Description + } + + e.UpdateAt = time.Now() +} diff --git a/service/team/model.go b/service/team/model.go new file mode 100644 index 00000000..37c975b7 --- /dev/null +++ b/service/team/model.go @@ -0,0 +1,43 @@ +package team + +import ( + "time" + + "github.com/APIParkLab/APIPark/stores/team" +) + +type Team struct { + Id string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + //Master string `json:"master"` + Organization string `json:"organization"` + CreateTime time.Time `json:"create_time"` + UpdateTime time.Time `json:"update_time"` + Creator string `json:"creator"` + Updater string `json:"updater"` +} + +func FromEntity(e *team.Team) *Team { + return &Team{ + Id: e.UUID, + Name: e.Name, + Description: e.Description, + //Master: e.Master, + CreateTime: e.CreateAt, + UpdateTime: e.UpdateAt, + Creator: e.Creator, + Updater: e.Updater, + } +} + +type CreateTeam struct { + Id string `json:"id" ` + Name string `json:"name" ` + Description string `json:"description"` +} +type EditTeam struct { + Name *string `json:"name" ` + Description *string `json:"description"` + //Master *string `json:"master" ` +} diff --git a/service/team/service.go b/service/team/service.go new file mode 100644 index 00000000..1408733e --- /dev/null +++ b/service/team/service.go @@ -0,0 +1,21 @@ +package team + +import ( + "reflect" + + "github.com/APIParkLab/APIPark/service/universally" + "github.com/eolinker/go-common/autowire" +) + +type ITeamService interface { + universally.IServiceGet[Team] + universally.IServiceDelete + universally.IServiceCreate[CreateTeam] + universally.IServiceEdit[EditTeam] +} + +func init() { + autowire.Auto[ITeamService](func() reflect.Value { + return reflect.ValueOf(new(imlTeamService)) + }) +} diff --git a/service/universally/com.go b/service/universally/com.go new file mode 100644 index 00000000..c512c155 --- /dev/null +++ b/service/universally/com.go @@ -0,0 +1,17 @@ +package universally + +import "github.com/eolinker/go-common/store" + +const ( + SoftDeleteField = "is_delete" + SoftDeleteWhere = "is_delete = false" +) + +func assert(e any) { + if _, ok := e.(store.Table); !ok { + panic("not implement store.Table") + } +} +func idValue(v any) int64 { + return (v.(store.Table)).IdValue() +} diff --git a/service/universally/commit/iml.go b/service/universally/commit/iml.go new file mode 100644 index 00000000..fa7c6bcf --- /dev/null +++ b/service/universally/commit/iml.go @@ -0,0 +1,109 @@ +package commit + +import ( + "context" + "github.com/eolinker/go-common/utils" + "gorm.io/gorm" + + "github.com/APIParkLab/APIPark/stores/universally/commit" +) + +var ( + _ ICommitWithKeyService[any] = (*imlCommitWithKeyService[any])(nil) + _ ICommitService[any] = (*imlCommitService[any])(nil) +) + +type imlCommitWithKeyService[T any] struct { + store commit.ICommitWKStore[T] `autowired:""` +} + +func (i *imlCommitWithKeyService[T]) List(ctx context.Context, uuids ...string) ([]*Commit[T], error) { + + list, err := i.store.List(ctx, uuids...) + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, newCommit[T]), nil +} + +func (i *imlCommitWithKeyService[T]) ListLatest(ctx context.Context, target ...string) ([]*Commit[T], error) { + list, err := i.store.Latest(ctx, target...) + if err != nil { + return nil, err + } + + return utils.SliceToSlice(list, newCommit[T]), nil +} + +func (i *imlCommitWithKeyService[T]) Get(ctx context.Context, uuid string) (*Commit[T], error) { + r, err := i.store.Get(ctx, uuid) + if err != nil { + return nil, err + } + + return newCommit(r), nil +} + +func (i *imlCommitWithKeyService[T]) Latest(ctx context.Context, target string) (*Commit[T], error) { + list, err := i.ListLatest(ctx, target) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, gorm.ErrRecordNotFound + } + + result := list[0] + return result, nil +} + +func (i *imlCommitWithKeyService[T]) Save(ctx context.Context, target string, data *T) error { + return i.store.Save(ctx, target, data) +} + +type imlCommitService[T any] struct { + store commit.ICommitStore[T] `autowired:""` +} + +func (i *imlCommitService[T]) List(ctx context.Context, uuids ...string) ([]*Commit[T], error) { + list, err := i.store.List(ctx, uuids...) + if err != nil { + return nil, err + } + + return utils.SliceToSlice(list, newCommit[T]), nil + +} + +func (i *imlCommitService[T]) ListLatest(ctx context.Context, target ...string) ([]*Commit[T], error) { + list, err := i.store.Latest(ctx, "", target...) + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, newCommit[T]), nil +} + +func (i *imlCommitService[T]) Get(ctx context.Context, uuid string) (*Commit[T], error) { + r, err := i.store.Get(ctx, uuid) + if err != nil { + return nil, err + } + + return newCommit(r), nil +} + +func (i *imlCommitService[T]) Latest(ctx context.Context, target string, key string) (*Commit[T], error) { + list, err := i.store.Latest(ctx, key, target) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, gorm.ErrRecordNotFound + } + result := list[0] + return newCommit(result), nil +} + +func (i *imlCommitService[T]) Save(ctx context.Context, target string, key string, data *T) error { + return i.store.Save(ctx, key, target, data) +} diff --git a/service/universally/commit/model.go b/service/universally/commit/model.go new file mode 100644 index 00000000..d7a7f0ee --- /dev/null +++ b/service/universally/commit/model.go @@ -0,0 +1,26 @@ +package commit + +import ( + "github.com/APIParkLab/APIPark/stores/universally/commit" + "time" +) + +type Commit[H any] struct { + UUID string + Target string + Key string + Data *H + CreateAt time.Time + Operator string +} + +func newCommit[H any](e *commit.Commit[H]) *Commit[H] { + return &Commit[H]{ + UUID: e.UUID, + Target: e.Target, + Key: e.Key, + Data: e.Data, + CreateAt: e.CreateAt, + Operator: e.Operator, + } +} diff --git a/service/universally/commit/service.go b/service/universally/commit/service.go new file mode 100644 index 00000000..5c5cbda4 --- /dev/null +++ b/service/universally/commit/service.go @@ -0,0 +1,44 @@ +package commit + +import ( + "context" + "reflect" + + "github.com/APIParkLab/APIPark/stores/universally/commit" + "github.com/eolinker/go-common/autowire" +) + +type ICommitWithKeyService[T any] interface { + Latest(ctx context.Context, target string) (*Commit[T], error) + ListLatest(ctx context.Context, target ...string) ([]*Commit[T], error) + Save(ctx context.Context, target string, data *T) error + Get(ctx context.Context, uuid string) (*Commit[T], error) + List(ctx context.Context, uuids ...string) ([]*Commit[T], error) +} + +func InitCommitWithKeyService[T any](name string, key string) { + autowire.Auto[commit.ICommitWKStore[T]](func() reflect.Value { + + return reflect.ValueOf(commit.NewCommitWithKey[T](name, key)) + }) + autowire.Auto[ICommitWithKeyService[T]](func() reflect.Value { + return reflect.ValueOf(&imlCommitWithKeyService[T]{}) + }) +} + +type ICommitService[T any] interface { + Latest(ctx context.Context, target string, key string) (*Commit[T], error) + ListLatest(ctx context.Context, target ...string) ([]*Commit[T], error) + Save(ctx context.Context, target string, key string, data *T) error + Get(ctx context.Context, uuid string) (*Commit[T], error) + List(ctx context.Context, uuids ...string) ([]*Commit[T], error) +} + +func InitCommitService[T any](name string) { + autowire.Auto[commit.ICommitStore[T]](func() reflect.Value { + return reflect.ValueOf(commit.NewCommitStore[T](name)) + }) + autowire.Auto[ICommitService[T]](func() reflect.Value { + return reflect.ValueOf(&imlCommitService[T]{}) + }) +} diff --git a/service/universally/create-softDelete.go b/service/universally/create-softDelete.go new file mode 100644 index 00000000..d790cd8f --- /dev/null +++ b/service/universally/create-softDelete.go @@ -0,0 +1,78 @@ +package universally + +import ( + "context" + "errors" + "fmt" + "github.com/eolinker/go-common/utils" + + "github.com/eolinker/go-common/auto" + "github.com/eolinker/go-common/store" + "gorm.io/gorm" +) + +type imlServiceCreateSoftDelete[INPUT any, E any] struct { + store store.ISearchStore[E] + createEntityHandler func(i *INPUT) *E + labelHandler []func(*E) []string + uniquestHandler func(*INPUT) []map[string]any + + name string +} + +func NewCreatorSoftDelete[INPUT any, E any](st store.ISearchStore[E], name string, createEntityHandler func(i *INPUT) *E, uniquestHandler func(*INPUT) []map[string]any, labelHandlers ...func(*E) []string) IServiceCreate[INPUT] { + + assert(new(E)) + + s := &imlServiceCreateSoftDelete[INPUT, E]{ + store: st, + createEntityHandler: createEntityHandler, + labelHandler: labelHandlers, + uniquestHandler: uniquestHandler, + + name: name, + } + return s +} + +func (p *imlServiceCreateSoftDelete[INPUT, E]) Create(ctx context.Context, input *INPUT, appendLabels ...string) error { + operator := utils.UserId(ctx) + uniquest := p.uniquestHandler(input) + + err := p.store.Transaction(ctx, func(ctx context.Context) error { + if uniquest != nil { + // check + + for _, v := range uniquest { + + v["is_delete"] = false + + o, err := p.store.First(ctx, v) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if o != nil { + return fmt.Errorf("%s %v already exists", p.name, v) + } + } + } + + ne := p.createEntityHandler(input) + auto.Auto("creator", operator, ne) + auto.Auto("updater", operator, ne) + err := p.store.Insert(ctx, ne) + if err != nil { + return err + } + labels := appendLabels + for _, hd := range p.labelHandler { + labels = append(labels, hd(ne)...) + } + return p.store.SetLabels(ctx, idValue(ne), labels...) + //return p.store.SetLabels(ctx, ne.Id, pn.UUID, pn.Name, pn.Url, pn.Prefix, pn.Resume) + }) + if err != nil { + return err + } + return nil +} diff --git a/service/universally/create.go b/service/universally/create.go new file mode 100644 index 00000000..e5b7b9c9 --- /dev/null +++ b/service/universally/create.go @@ -0,0 +1,77 @@ +package universally + +import ( + "context" + "errors" + "fmt" + "github.com/eolinker/go-common/utils" + + "github.com/eolinker/go-common/auto" + "github.com/eolinker/go-common/store" + "gorm.io/gorm" +) + +type IServiceCreate[INPUT any] interface { + Create(ctx context.Context, input *INPUT, appendLabels ...string) (err error) +} + +type imlServiceCreate[INPUT any, E any] struct { + store store.ISearchStore[E] + createEntityHandler func(i *INPUT) *E + labelHandler []func(*E) []string + uniquestHandler func(*INPUT) []map[string]any + + name string +} + +func NewCreator[INPUT any, E any](st store.ISearchStore[E], name string, createEntityHandler func(i *INPUT) *E, uniquestHandler func(*INPUT) []map[string]any, labelHandler ...func(*E) []string) IServiceCreate[INPUT] { + assert(new(E)) + + return &imlServiceCreate[INPUT, E]{ + store: st, + createEntityHandler: createEntityHandler, + labelHandler: labelHandler, + uniquestHandler: uniquestHandler, + name: name, + } +} + +func (p *imlServiceCreate[INPUT, E]) Create(ctx context.Context, input *INPUT, appendLabels ...string) error { + operator := utils.UserId(ctx) + uniquest := p.uniquestHandler(input) + + err := p.store.Transaction(ctx, func(ctx context.Context) error { + if uniquest != nil { + // check + + for _, v := range uniquest { + + o, err := p.store.First(ctx, v) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if o != nil { + return fmt.Errorf("%s %v already exists", p.name, v) + } + } + } + + ne := p.createEntityHandler(input) + auto.Auto("creator", operator, ne) + auto.Auto("updater", operator, ne) + err := p.store.Insert(ctx, ne) + if err != nil { + return err + } + labels := appendLabels + for _, hd := range p.labelHandler { + labels = append(labels, hd(ne)...) + } + return p.store.SetLabels(ctx, idValue(ne), labels...) + //return p.store.SetLabels(ctx, ne.Id, pn.UUID, pn.Name, pn.Url, pn.Prefix, pn.Resume) + }) + if err != nil { + return err + } + return nil +} diff --git a/service/universally/delete.go b/service/universally/delete.go new file mode 100644 index 00000000..565237a9 --- /dev/null +++ b/service/universally/delete.go @@ -0,0 +1,72 @@ +package universally + +import ( + "context" + "errors" + "fmt" + "github.com/eolinker/go-common/auto" + "github.com/eolinker/go-common/store" + "github.com/eolinker/go-common/utils" + "gorm.io/gorm" +) + +var ( + _ IServiceDelete = (*imlServiceDelete[any])(nil) +) + +type IServiceDelete interface { + Delete(ctx context.Context, uuid string) error +} + +type imlServiceDelete[E any] struct { + store store.ISearchStore[E] +} + +func NewDelete[E any](store store.ISearchStore[E]) IServiceDelete { + assert(new(E)) + return &imlServiceDelete[E]{store: store} +} +func NewSoftDelete[E any](s store.ISearchStore[E]) IServiceDelete { + assert(new(E)) + return &imlServiceSoftDelete[E]{store: s} +} +func (p *imlServiceDelete[E]) Delete(ctx context.Context, uuid string) error { + return p.store.Transaction(ctx, func(ctx context.Context) error { + o, err := p.store.First(ctx, map[string]interface{}{"uuid": uuid}) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if o == nil { + return fmt.Errorf("partition %s not exists", uuid) + } + + _, err = p.store.DeleteWhere(ctx, map[string]interface{}{"uuid": uuid}) + if err != nil { + return err + } + return p.store.SetLabels(ctx, idValue(o)) + }) + +} + +type imlServiceSoftDelete[E any] struct { + store store.ISearchStore[E] +} + +func (p *imlServiceSoftDelete[E]) Delete(ctx context.Context, uuid string) error { + operator := utils.UserId(ctx) + return p.store.Transaction(ctx, func(ctx context.Context) error { + o, err := p.store.First(ctx, map[string]interface{}{"uuid": uuid}) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if o == nil { + return fmt.Errorf("partition %s not exists", uuid) + } + + auto.Auto("operator", operator, o) + return p.store.SoftDelete(ctx, map[string]interface{}{"uuid": uuid}) + + }) + +} diff --git a/service/universally/edit.go b/service/universally/edit.go new file mode 100644 index 00000000..298f40dd --- /dev/null +++ b/service/universally/edit.go @@ -0,0 +1,55 @@ +package universally + +import ( + "context" + "github.com/eolinker/go-common/utils" + + "github.com/eolinker/go-common/auto" + "github.com/eolinker/go-common/store" +) + +var ( + _ IServiceEdit[any] = (*imlServiceEdit[any, any])(nil) +) + +type IServiceEdit[INPUT any] interface { + Save(ctx context.Context, id string, model *INPUT, appendLabels ...string) error +} + +type imlServiceEdit[INPUT any, E any] struct { + store store.ISearchStore[E] + updateHandler func(e *E, model *INPUT) + labelHandler []func(model *E) []string +} + +func NewEdit[INPUT any, E any](st store.ISearchStore[E], updateHandler func(e *E, model *INPUT), labels ...func(model *E) []string) IServiceEdit[INPUT] { + assert(new(E)) + + return &imlServiceEdit[INPUT, E]{store: st, updateHandler: updateHandler, labelHandler: labels} +} + +func (p *imlServiceEdit[INPUT, E]) Save(ctx context.Context, id string, model *INPUT, appendLabels ...string) error { + operator := utils.UserId(ctx) + return p.store.Transaction(ctx, func(ctx context.Context) error { + ev, err := p.store.First(ctx, map[string]interface{}{ + "uuid": id, + }) + if err != nil { + return err + } + + auto.Auto("updater", operator, ev) + p.updateHandler(ev, model) + err = p.store.Save(ctx, ev) + if err != nil { + return err + } + labels := appendLabels + for _, hd := range p.labelHandler { + labels = append(labels, hd(ev)...) + } + + return p.store.SetLabels(ctx, idValue(ev), labels...) + + }) +} diff --git a/service/universally/get-softdelete.go b/service/universally/get-softdelete.go new file mode 100644 index 00000000..e191fcf6 --- /dev/null +++ b/service/universally/get-softdelete.go @@ -0,0 +1,90 @@ +package universally + +import ( + "context" + "strings" + + "github.com/eolinker/go-common/store" + "github.com/eolinker/go-common/utils" +) + +var ( + _ IServiceGet[any] = (*imlServiceGetSoftDelete[any, any])(nil) +) + +type imlServiceGetSoftDelete[T any, E any] struct { + store store.ISearchStore[E] + toModelHandler func(*E) *T +} + +func NewGetSoftDelete[T any, E any](store store.ISearchStore[E], toModelHandler func(*E) *T) IServiceGet[T] { + + return &imlServiceGetSoftDelete[T, E]{store: store, toModelHandler: toModelHandler} + +} +func (s *imlServiceGetSoftDelete[T, E]) Get(ctx context.Context, uuid string) (*T, error) { + where := map[string]interface{}{ + "uuid": uuid, + } + + where[SoftDeleteField] = false + + v, err := s.store.First(ctx, where) + if err != nil { + return nil, err + } + return s.toModelHandler(v), nil +} +func (s *imlServiceGetSoftDelete[T, E]) List(ctx context.Context, uuids ...string) ([]*T, error) { + where := make([]string, 0, 2) + args := make([]interface{}, 0, 2) + + where = append(where, SoftDeleteWhere) + + if len(uuids) > 0 { + if len(uuids) == 1 { + where = append(where, "uuid = ?") + args = append(args, uuids[0]) + } else { + where = append(where, "uuid in ?") + args = append(args, uuids) + } + } + + list, err := s.store.ListQuery(ctx, strings.Join(where, " and "), args, "name asc") + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, s.toModelHandler), nil +} +func (s *imlServiceGetSoftDelete[T, E]) Search(ctx context.Context, keyword string, condition map[string]interface{}, sortRule ...string) ([]*T, error) { + if condition == nil { + condition = make(map[string]interface{}) + } + condition[SoftDeleteField] = false + ps, err := s.store.Search(ctx, keyword, condition, sortRule...) + if err != nil { + return nil, err + } + return utils.SliceToSlice(ps, s.toModelHandler), nil +} + +func (s *imlServiceGetSoftDelete[T, E]) SearchByPage(ctx context.Context, keyword string, condition map[string]interface{}, page int, pageSize int, sortRule ...string) ([]*T, int64, error) { + if condition == nil { + condition = make(map[string]interface{}) + } + condition[SoftDeleteField] = false + ps, total, err := s.store.SearchByPage(ctx, keyword, condition, page, pageSize, sortRule...) + if err != nil { + return nil, 0, err + } + return utils.SliceToSlice(ps, s.toModelHandler), total, nil +} + +func (s *imlServiceGetSoftDelete[T, E]) Count(ctx context.Context, keyword string, condition map[string]interface{}) (int64, error) { + return s.store.Count(ctx, keyword, condition) +} + +func (s *imlServiceGetSoftDelete[T, E]) CountByGroup(ctx context.Context, keyword string, condition map[string]interface{}, groupBy string) (map[string]int64, error) { + return s.store.CountByGroup(ctx, keyword, condition, groupBy) +} diff --git a/service/universally/get.go b/service/universally/get.go new file mode 100644 index 00000000..63aaabc5 --- /dev/null +++ b/service/universally/get.go @@ -0,0 +1,95 @@ +package universally + +import ( + "context" + "errors" + "strings" + + "gorm.io/gorm" + + "github.com/eolinker/go-common/store" + "github.com/eolinker/go-common/utils" +) + +var ( + _ IServiceGet[any] = (*imlServiceGet[any, any])(nil) +) + +type IServiceGet[T any] interface { + Get(ctx context.Context, uuid string) (*T, error) + List(ctx context.Context, uuids ...string) ([]*T, error) + + Search(ctx context.Context, keyword string, condition map[string]interface{}, sortRule ...string) ([]*T, error) + Count(ctx context.Context, keyword string, condition map[string]interface{}) (int64, error) + CountByGroup(ctx context.Context, keyword string, condition map[string]interface{}, groupBy string) (map[string]int64, error) + SearchByPage(ctx context.Context, keyword string, condition map[string]interface{}, page int, pageSize int, sortRule ...string) ([]*T, int64, error) +} + +type imlServiceGet[T any, E any] struct { + store store.ISearchStore[E] + toModelHandler func(*E) *T +} + +func NewGet[T any, E any](store store.ISearchStore[E], toModelHandler func(*E) *T) IServiceGet[T] { + + return &imlServiceGet[T, E]{store: store, toModelHandler: toModelHandler} +} + +func (s *imlServiceGet[T, E]) Get(ctx context.Context, uuid string) (*T, error) { + where := map[string]interface{}{ + "uuid": uuid, + } + + v, err := s.store.First(ctx, where) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + if v == nil || errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.New("not found") + } + + return s.toModelHandler(v), nil +} +func (s *imlServiceGet[T, E]) List(ctx context.Context, uuids ...string) ([]*T, error) { + where := make([]string, 0, 2) + args := make([]interface{}, 0, 2) + + if len(uuids) > 0 { + if len(uuids) == 1 { + where = append(where, "uuid = ?") + args = append(args, uuids[0]) + } else { + where = append(where, "uuid in ?") + args = append(args, uuids) + } + } + + list, err := s.store.ListQuery(ctx, strings.Join(where, " and "), args, "name asc") + if err != nil { + return nil, err + } + return utils.SliceToSlice(list, s.toModelHandler), nil +} +func (s *imlServiceGet[T, E]) Search(ctx context.Context, keyword string, condition map[string]interface{}, sortRule ...string) ([]*T, error) { + ps, err := s.store.Search(ctx, keyword, condition, sortRule...) + if err != nil { + return nil, err + } + return utils.SliceToSlice(ps, s.toModelHandler), nil +} + +func (s *imlServiceGet[T, E]) SearchByPage(ctx context.Context, keyword string, condition map[string]interface{}, page int, pageSize int, sortRule ...string) ([]*T, int64, error) { + ps, total, err := s.store.SearchByPage(ctx, keyword, condition, page, pageSize, sortRule...) + if err != nil { + return nil, 0, err + } + return utils.SliceToSlice(ps, s.toModelHandler), total, nil +} + +func (s *imlServiceGet[T, E]) Count(ctx context.Context, keyword string, condition map[string]interface{}) (int64, error) { + return s.store.Count(ctx, keyword, condition) +} + +func (s *imlServiceGet[T, E]) CountByGroup(ctx context.Context, keyword string, condition map[string]interface{}, groupBy string) (map[string]int64, error) { + return s.store.CountByGroup(ctx, keyword, condition, groupBy) +} diff --git a/service/upstream/iml.go b/service/upstream/iml.go new file mode 100644 index 00000000..343347de --- /dev/null +++ b/service/upstream/iml.go @@ -0,0 +1,101 @@ +package upstream + +import ( + "context" + "errors" + "time" + + "github.com/APIParkLab/APIPark/service/universally/commit" + "github.com/APIParkLab/APIPark/stores/upstream" + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/utils" +) + +var ( + _ IUpstreamService = (*imlUpstreamService)(nil) + _ autowire.Complete = (*imlUpstreamService)(nil) +) + +type imlUpstreamService struct { + store upstream.IUpstreamStore `autowired:""` + commitService commit.ICommitService[Config] `autowired:""` +} + +func (i *imlUpstreamService) ListCommit(ctx context.Context, uuid ...string) ([]*commit.Commit[Config], error) { + return i.commitService.List(ctx, uuid...) +} + +func (i *imlUpstreamService) ListLatestCommit(ctx context.Context, project string) ([]*commit.Commit[Config], error) { + upstreams, err := i.store.List(ctx, map[string]interface{}{ + "project": project, + }) + if err != nil { + return nil, err + } + if len(upstreams) == 0 { + return nil, errors.New("upstream not found") + } + targetId := utils.SliceToSlice(upstreams, func(u *upstream.Upstream) string { + return u.UUID + }) + return i.commitService.ListLatest(ctx, targetId...) + +} + +func (i *imlUpstreamService) GetCommit(ctx context.Context, uuid string) (*commit.Commit[Config], error) { + return i.commitService.Get(ctx, uuid) +} + +func (i *imlUpstreamService) LatestCommit(ctx context.Context, uid string, partition string) (*commit.Commit[Config], error) { + + return i.commitService.Latest(ctx, uid, partition) +} + +func (i *imlUpstreamService) SaveCommit(ctx context.Context, uid string, partition string, cfg *Config) error { + return i.commitService.Save(ctx, uid, partition, cfg) +} + +func (i *imlUpstreamService) OnComplete() { + +} + +func (i *imlUpstreamService) Get(ctx context.Context, id string) (*Upstream, error) { + t, err := i.store.First(ctx, map[string]interface{}{"uuid": id}) + if err != nil { + return nil, err + } + + return &Upstream{ + Item: &Item{ + UUID: t.UUID, + Project: t.Project, + Team: t.Team, + Remark: t.Remark, + Creator: t.Creator, + Updater: t.Updater, + CreateTime: t.CreateAt, + UpdateTime: t.UpdateAt, + }, + }, nil +} + +func (i *imlUpstreamService) Save(ctx context.Context, u *SaveUpstream) error { + now := time.Now() + userId := utils.UserId(ctx) + return i.store.Save(ctx, &upstream.Upstream{ + UUID: u.UUID, + Name: u.Name, + Project: u.Project, + Team: u.Team, + Remark: u.Remark, + Creator: userId, + Updater: userId, + CreateAt: now, + UpdateAt: now, + }) +} + +func (i *imlUpstreamService) Delete(ctx context.Context, id string) error { + _, err := i.store.DeleteWhere(ctx, map[string]interface{}{"uuid": id}) + return err +} diff --git a/service/upstream/model.go b/service/upstream/model.go new file mode 100644 index 00000000..5e9ee871 --- /dev/null +++ b/service/upstream/model.go @@ -0,0 +1,61 @@ +package upstream + +import ( + "time" +) + +type Item struct { + UUID string + Name string + Type string + Project string + Team string + Creator string + Updater string + Remark string + CreateTime time.Time + UpdateTime time.Time +} + +type Upstream struct { + *Item +} + +type SaveUpstream struct { + UUID string + Name string + Project string + Team string + //Type string + Remark string +} + +type ProxyHeader struct { + Key string `json:"key,omitempty"` + Value string `json:"value,omitempty"` + OptType string `json:"optType,omitempty"` +} + +type NodeConfig struct { + Address string `json:"address,omitempty"` + Weight int `json:"weight,omitempty"` +} + +type DiscoverConfig struct { + Service string `json:"service,omitempty"` + Discover string `json:"discover,omitempty"` +} + +type Config struct { + Balance string `json:"balance,omitempty"` + Timeout int `json:"timeout,omitempty"` + Retry int `json:"retry,omitempty"` + Type string `json:"type,omitempty"` + LimitPeerSecond int `json:"limit_peer_second,omitempty"` + ProxyHeaders []*ProxyHeader `json:"proxy_headers,omitempty"` + Scheme string `json:"scheme"` + PassHost string `json:"pass_host"` + UpstreamHost string `json:"upstream_host"` + Nodes []*NodeConfig `json:"nodes"` + Discover *DiscoverConfig `json:"discover"` +} diff --git a/service/upstream/service.go b/service/upstream/service.go new file mode 100644 index 00000000..404aac16 --- /dev/null +++ b/service/upstream/service.go @@ -0,0 +1,28 @@ +package upstream + +import ( + "context" + "reflect" + + "github.com/APIParkLab/APIPark/service/universally/commit" + "github.com/eolinker/go-common/autowire" +) + +type IUpstreamService interface { + Get(ctx context.Context, id string) (*Upstream, error) + Save(ctx context.Context, upstream *SaveUpstream) error + Delete(ctx context.Context, id string) error + LatestCommit(ctx context.Context, uid string, partition string) (*commit.Commit[Config], error) + ListLatestCommit(ctx context.Context, project string) ([]*commit.Commit[Config], error) + SaveCommit(ctx context.Context, uid string, partition string, cfg *Config) error + GetCommit(ctx context.Context, uuid string) (*commit.Commit[Config], error) + ListCommit(ctx context.Context, uuid ...string) ([]*commit.Commit[Config], error) +} + +func init() { + autowire.Auto[IUpstreamService](func() reflect.Value { + return reflect.ValueOf(new(imlUpstreamService)) + }) + commit.InitCommitService[Config]("upstream") + +} diff --git a/stores/api/api.go b/stores/api/api.go new file mode 100644 index 00000000..9713f47d --- /dev/null +++ b/stores/api/api.go @@ -0,0 +1,26 @@ +package api + +import ( + "reflect" + + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/store" +) + +type IApiBaseStore interface { + store.ISearchStore[Api] +} +type IAPIInfoStore store.IBaseStore[Info] +type imlApiBaseStore struct { + store.SearchStoreSoftDelete[Api] +} + +func init() { + + autowire.Auto[IApiBaseStore](func() reflect.Value { + return reflect.ValueOf(new(imlApiBaseStore)) + }) + autowire.Auto[IAPIInfoStore](func() reflect.Value { + return reflect.ValueOf(new(store.Store[Info])) + }) +} diff --git a/stores/api/model.go b/stores/api/model.go new file mode 100644 index 00000000..c245b213 --- /dev/null +++ b/stores/api/model.go @@ -0,0 +1,47 @@ +package api + +import "time" + +type Api struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Name string `gorm:"type:varchar(100);not null;column:name;comment:name"` + Driver string `gorm:"size:36;not null;column:driver;comment:驱动;index:driver"` // 驱动 + Service string `gorm:"size:36;not null;column:service;comment:服务;index:service"` // 服务 + Team string `gorm:"size:36;not null;column:team;comment:团队;index:team"` // 团队id + Creator string `gorm:"size:36;not null;column:creator;comment:创建人;index:creator" aovalue:"creator"` // 创建人 + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` + IsDelete int `gorm:"type:tinyint(1);not null;column:is_delete;comment:是否删除 0:未删除 1:已删除"` + Method string `gorm:"size:36;not null;column:method;comment:请求方法"` + Path string `gorm:"size:512;not null;column:path;comment:请求路径"` +} +type Info struct { + Id int64 `gorm:"column:id;type:BIGINT(20);NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Name string `gorm:"type:varchar(100);not null;column:name;comment:name"` + Description string `gorm:"size:255;not null;column:description;comment:description"` + Service string `gorm:"size:36;not null;column:service;comment:服务;index:service"` + Team string `gorm:"size:36;not null;column:team;comment:团队;index:team"` // 团队id + Method string `gorm:"size:36;not null;column:method;comment:请求方法"` + Path string `gorm:"size:512;not null;column:path;comment:请求路径"` + Match string `gorm:"type:text;null;column:match;comment:匹配规则"` + Creator string `gorm:"size:36;not null;column:creator;comment:创建人;index:creator" aovalue:"creator"` // 创建人 + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` + Updater string `gorm:"size:36;not null;column:updater;comment:更新人;index:updater" aovalue:"updater"` // 更新人 + UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:update_at;comment:更新时间"` +} + +func (i *Info) TableName() string { + return "api_info" +} + +func (i *Info) IdValue() int64 { + return i.Id +} + +func (a *Api) IdValue() int64 { + return a.Id +} +func (a *Api) TableName() string { + return "api" +} diff --git a/stores/base.go b/stores/base.go new file mode 100644 index 00000000..8209f73d --- /dev/null +++ b/stores/base.go @@ -0,0 +1 @@ +package stores diff --git a/stores/catalogue/model.go b/stores/catalogue/model.go new file mode 100644 index 00000000..00768a88 --- /dev/null +++ b/stores/catalogue/model.go @@ -0,0 +1,22 @@ +package catalogue + +import "time" + +type Catalogue struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Name string `gorm:"type:varchar(100);not null;column:name;comment:name"` + Sort int `gorm:"type:int;not null;column:sort;comment:排序"` + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` + UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;column:update_at;comment:修改时间" json:"update_at"` + + Parent string `gorm:"size:36;not null;column:parent;comment:父组id"` +} + +func (o *Catalogue) TableName() string { + return "catalogue" +} + +func (o *Catalogue) IdValue() int64 { + return o.Id +} diff --git a/stores/catalogue/store.go b/stores/catalogue/store.go new file mode 100644 index 00000000..14bb3d88 --- /dev/null +++ b/stores/catalogue/store.go @@ -0,0 +1,22 @@ +package catalogue + +import ( + "reflect" + + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/store" +) + +type ICatalogueStore interface { + store.ISearchStore[Catalogue] +} + +type imlCatalogueStore struct { + store.SearchStore[Catalogue] +} + +func init() { + autowire.Auto[ICatalogueStore](func() reflect.Value { + return reflect.ValueOf(new(imlCatalogueStore)) + }) +} diff --git a/stores/certificate/model.go b/stores/certificate/model.go new file mode 100644 index 00000000..bf42b6a1 --- /dev/null +++ b/stores/certificate/model.go @@ -0,0 +1,36 @@ +package certificate + +import "time" + +type Certificate struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Name string `gorm:"type:varchar(100);not null;column:name;comment:name"` + Cluster string `gorm:"type:varchar(36);not null;column:cluster;comment:集群;index:cluster"` + Domains []string `gorm:"type:text;not null;column:domains;comment:域名;serializer:json"` + NotBefore time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:not_before;comment:生效时间"` + NotAfter time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:not_after;comment:失效时间"` + Updater string `gorm:"size:36;not null;column:updater;comment:更新人;index:updater"` // 更新人 + UpdateTime time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;column:update_at;comment:修改时间"` +} + +func (c *Certificate) IdValue() int64 { + return c.Id +} +func (c *Certificate) TableName() string { + return "certificate" +} + +type File struct { + Id int64 `gorm:"column:id;type:BIGINT(20);NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Key []byte `gorm:"type:blob;not null;column:key;comment:证书key"` + Cert []byte `gorm:"type:blob;not null;column:cert;comment:证书cert"` +} + +func (f *File) IdValue() int64 { + return f.Id +} +func (f *File) TableName() string { + return "certificate_file" +} diff --git a/stores/certificate/store.go b/stores/certificate/store.go new file mode 100644 index 00000000..b7954bc0 --- /dev/null +++ b/stores/certificate/store.go @@ -0,0 +1,32 @@ +package certificate + +import ( + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/store" + "reflect" +) + +type ICertificateStore interface { + store.IBaseStore[Certificate] +} +type ICertificateFileStore interface { + store.IBaseStore[File] +} + +type imlCertificateStore struct { + store.Store[Certificate] +} + +type imlCertificateFileStore struct { + store.Store[File] +} + +func init() { + autowire.Auto[ICertificateStore](func() reflect.Value { + return reflect.ValueOf(new(imlCertificateStore)) + }) + autowire.Auto[ICertificateFileStore](func() reflect.Value { + return reflect.ValueOf(new(imlCertificateFileStore)) + }) + +} diff --git a/stores/cluster/cluster.go b/stores/cluster/cluster.go new file mode 100644 index 00000000..5b7d65e8 --- /dev/null +++ b/stores/cluster/cluster.go @@ -0,0 +1,43 @@ +package cluster + +import ( + "context" + + "github.com/eolinker/go-common/store" +) + +var ( + _ IClusterStore = (*storeCluster)(nil) +) + +type countByPartition struct { + Partition string + Count int +} +type IClusterStore interface { + store.IBaseStore[Cluster] + Count(ctx context.Context) (map[string]int, error) +} +type storeCluster struct { + store.Store[Cluster] // 用struct方式继承,会自动填充并初始化表 +} + +func (s *storeCluster) Count(ctx context.Context) (map[string]int, error) { + rows, err := s.DB(ctx).Model(&Cluster{}).Select([]string{`partition`, "count(*)"}).Group("partition").Rows() + if err != nil { + return nil, err + } + defer rows.Close() + + rs := make(map[string]int) + for rows.Next() { + var partition string + var count int + err := rows.Scan(&partition, &count) + if err != nil { + return nil, err + } + rs[partition] = count + } + return rs, nil +} diff --git a/stores/cluster/model.go b/stores/cluster/model.go new file mode 100644 index 00000000..abd3e459 --- /dev/null +++ b/stores/cluster/model.go @@ -0,0 +1,53 @@ +package cluster + +import "time" + +type Cluster struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Name string `gorm:"type:varchar(100);not null;column:name;comment:name"` + //Cluster string `gorm:"type:varchar(36);not null;column:partition;comment:partition"` + Resume string `gorm:"type:varchar(255);not null;column:resume;comment:resume"` + Creator string `gorm:"type:varchar(36);not null;column:creator;comment:creator" aovalue:"creator"` + Updater string `gorm:"type:varchar(36);not null;column:updater;comment:updater" aovalue:"updater"` + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` + UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;column:update_at;comment:修改时间" json:"update_at"` +} + +func (c *Cluster) IdValue() int64 { + return c.Id +} +func (c *Cluster) TableName() string { + return "cluster" +} + +type Node struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"size:36;not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Name string `gorm:"size:100;not null;column:name;comment:name"` + Cluster string `gorm:"column:cluster;type:varchar(36);NOT NULL;comment:cluster id;"` + UpdateTime time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;column:update_at;comment:修改时间" json:"update_at"` +} + +func (c *Node) IdValue() int64 { + return c.Id +} +func (c *Node) TableName() string { + return "cluster_node" +} + +type NodeAddr struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + Cluster string `gorm:"column:cluster;type:varchar(36);NOT NULL;comment:cluster id;"` + Node string `gorm:"column:node;type:varchar(36);NOT NULL;comment:node id;"` + Type string `gorm:"size:32;not null;column:type;comment:type;uniqueIndex:node_type_addr;"` + Addr string `gorm:"size:255;not null;column:addr;comment:addr;uniqueIndex:node_type_addr;"` + UpdateTime time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;column:update_at;comment:修改时间" json:"update_at"` +} + +func (c *NodeAddr) IdValue() int64 { + return c.Id +} +func (c *NodeAddr) TableName() string { + return "cluster_node_addr" +} diff --git a/stores/cluster/store.go b/stores/cluster/store.go new file mode 100644 index 00000000..2c2e97bf --- /dev/null +++ b/stores/cluster/store.go @@ -0,0 +1,35 @@ +package cluster + +import ( + "reflect" + + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/store" +) + +type IClusterNodeStore interface { + store.IBaseStore[Node] +} +type storeClusterNode struct { + store.Store[Node] // 用struct方式继承,会自动填充并初始化表 +} +type IClusterNodeAddressStore interface { + store.IBaseStore[NodeAddr] +} +type storeClusterNodeAddr struct { + store.Store[NodeAddr] // 用struct方式继承,会自动填充并初始化表 +} + +func init() { + autowire.Auto[IClusterStore](func() reflect.Value { + return reflect.ValueOf(new(storeCluster)) + }) + + autowire.Auto[IClusterNodeStore](func() reflect.Value { + return reflect.ValueOf(new(storeClusterNode)) + }) + + autowire.Auto[IClusterNodeAddressStore](func() reflect.Value { + return reflect.ValueOf(new(storeClusterNodeAddr)) + }) +} diff --git a/stores/dynamic-module/model.go b/stores/dynamic-module/model.go new file mode 100644 index 00000000..54bf80ec --- /dev/null +++ b/stores/dynamic-module/model.go @@ -0,0 +1,46 @@ +package dynamic_module + +import "time" + +type DynamicModule struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Name string `gorm:"type:varchar(100);not null;column:name;comment:name"` + Driver string `gorm:"column:driver;type:VARCHAR(255);NOT NULL;comment:驱动"` + Description string `gorm:"column:description;type:VARCHAR(255);comment:描述"` + Version string `gorm:"column:version;type:VARCHAR(32);NOT NULL;comment:版本"` + Config string `gorm:"column:config;type:TEXT;NOT NULL;comment:配置"` + Module string `gorm:"column:module;type:VARCHAR(255);NOT NULL;comment:模块"` + Profession string `gorm:"column:profession;type:VARCHAR(255);NOT NULL;comment:插件指定profession"` + Skill string `gorm:"column:skill;type:VARCHAR(255);comment:模块提供能力"` + Creator string `gorm:"type:varchar(36);not null;column:creator;comment:creator" aovalue:"creator"` + Updater string `gorm:"type:varchar(36);not null;column:updater;comment:updater" aovalue:"updater"` + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` + UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;column:update_at;comment:修改时间"` +} + +func (c *DynamicModule) IdValue() int64 { + return c.Id +} +func (c *DynamicModule) TableName() string { + return "dynamic_module" +} + +type DynamicModulePublish struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + DynamicModule string `gorm:"column:dynamic_module;type:VARCHAR(255);NOT NULL;comment:动态模块ID"` + Module string `gorm:"column:module;type:VARCHAR(255);NOT NULL;comment:模块"` + Cluster string `gorm:"column:cluster;type:VARCHAR(255);NOT NULL;comment:集群"` + Version string `gorm:"column:version;type:VARCHAR(32);NOT NULL;comment:版本"` + Creator string `gorm:"type:varchar(36);not null;column:creator;comment:creator" aovalue:"creator"` + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` +} + +func (c *DynamicModulePublish) IdValue() int64 { + return c.Id +} + +func (c *DynamicModulePublish) TableName() string { + return "dynamic_module_publish" +} diff --git a/stores/dynamic-module/store.go b/stores/dynamic-module/store.go new file mode 100644 index 00000000..40117ef0 --- /dev/null +++ b/stores/dynamic-module/store.go @@ -0,0 +1,33 @@ +package dynamic_module + +import ( + "reflect" + + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/store" +) + +type IDynamicModuleStore interface { + store.ISearchStore[DynamicModule] +} +type storeDynamicModule struct { + store.SearchStore[DynamicModule] // 用struct方式继承,会自动填充并初始化表 +} + +type IDynamicModulePublishStore interface { + store.ISearchStore[DynamicModulePublish] +} + +type storeDynamicModulePublish struct { + store.SearchStore[DynamicModulePublish] // 用struct方式继承,会自动填充并初始化表 +} + +func init() { + autowire.Auto[IDynamicModuleStore](func() reflect.Value { + return reflect.ValueOf(new(storeDynamicModule)) + }) + + autowire.Auto[IDynamicModulePublishStore](func() reflect.Value { + return reflect.ValueOf(new(storeDynamicModulePublish)) + }) +} diff --git a/stores/plugin/model.go b/stores/plugin/model.go new file mode 100644 index 00000000..30ec06ce --- /dev/null +++ b/stores/plugin/model.go @@ -0,0 +1,45 @@ +package plugin + +import ( + "github.com/APIParkLab/APIPark/model/plugin_model" + "time" +) + +type Define struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + Extend string `gorm:"type:text;not null;column:extend;comment:扩展字段"` + Name string `gorm:"type:varchar(36);not null;column:name;uniqueIndex:name;comment:name;"` + Cname string `gorm:"type:varchar(100);not null;column:name;comment:cname"` + Description string `gorm:"size:255;not null;column:description;comment:描述"` + Kind plugin_model.Kind `gorm:"type:tinyint(2);not null;column:kind;comment:类型;index:kind;"` + Status plugin_model.Status `gorm:"type:tinyint(2);not null;column:status;comment:状态;index:status;"` + Render plugin_model.Render `gorm:"type:text;not null;column:render;comment:render;serializer:json"` + Config plugin_model.ConfigType `gorm:"type:text;not null;column:config;comment:配置; serializer:json"` + Sort int `gorm:"type:int(11);not null;column:sort;comment:排序"` + UpdateTime time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;column:update_at;comment:修改时间"` +} + +func (m *Define) IdValue() int64 { + return m.Id +} +func (m *Define) TableName() string { + return "plugin_define" +} + +type Partition struct { + Id int64 `gorm:"type:BIGINT(20);size:20;not null;auto_increment;primary_key;column:id;comment:主键ID;"` + Partition string `gorm:"size:36;not null;column:partition;comment:分区id;uniqueIndex:partition_plugin,partition_pluginId;index:partition"` + Plugin string `gorm:"size:36;not null;column:plugin;comment:插件name;uniqueIndex:partition_plugin;index:plugin"` + Config plugin_model.ConfigType `gorm:"type:text;not null;column:config;comment:配置; serializer:json"` + Status plugin_model.Status `gorm:"type:tinyint(2);not null;column:status;comment:状态;index:status;"` + CreateTime time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` + UpdateTime time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;column:update_at;comment:修改时间"` + Operator string `gorm:"type:varchar(36);not null;column:operator;comment:操作人;"` +} + +func (p *Partition) TableName() string { + return "plugin_partition" +} +func (p *Partition) IdValue() int64 { + return p.Id +} diff --git a/stores/plugin/store.go b/stores/plugin/store.go new file mode 100644 index 00000000..f3397629 --- /dev/null +++ b/stores/plugin/store.go @@ -0,0 +1,31 @@ +package plugin + +import ( + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/store" + "reflect" +) + +type IPluginDefineStore interface { + store.IBaseStore[Define] +} +type storePlugin struct { + store.Store[Define] +} + +type IPartitionPluginStore interface { + store.IBaseStore[Partition] +} + +type storePartition struct { + store.Store[Partition] +} + +func init() { + autowire.Auto[IPluginDefineStore](func() reflect.Value { + return reflect.ValueOf(new(storePlugin)) + }) + autowire.Auto[IPartitionPluginStore](func() reflect.Value { + return reflect.ValueOf(new(storePartition)) + }) +} diff --git a/stores/publish/model.go b/stores/publish/model.go new file mode 100644 index 00000000..d1af1749 --- /dev/null +++ b/stores/publish/model.go @@ -0,0 +1,68 @@ +package publish + +import "time" + +type Publish struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Service string `gorm:"type:varchar(50);not null;column:service;comment:服务名;index:service"` + Release string `gorm:"type:varchar(36);not null;column:release;comment:release id;"` + Previous string `gorm:"type:varchar(50);not null;column:previous;comment:上一个版本release id;index:previous"` + Version string `gorm:"type:varchar(50);not null;column:version;comment:版本号(冗余);index:version;"` + ApplyTime time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:apply_time;comment:申请时间"` + Applicant string `gorm:"size:36;not null;column:applicant;comment:申请人;index:applicant"` + Remark string `gorm:"type:text;not null;column:remark;comment:备注"` + ApproveTime time.Time `gorm:"type:timestamp;DEFAULT:CURRENT_TIMESTAMP;column:approve_time;comment:审批时间"` + Approver string `gorm:"size:36;not null;column:approver;comment:审批人;index:approver"` + Comments string `gorm:"type:text;not null;column:comments;comment:审批意见"` + Status int `gorm:"type:int(11);not null;column:status;index:status; comment:状态, 0: 申请中, 1: 审批中, 2: 审批通过, 3: 审批拒绝, 4: 已发布 5: 已中止 6: 已关闭 7: 发布中 8:发布失败"` +} + +func (t *Publish) IdValue() int64 { + return t.Id +} +func (t *Publish) TableName() string { + return "service_publish" +} + +type Diff struct { + Id int64 `gorm:"column:id;type:BIGINT(20);NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;comment:UUID;index:uuid"` + Data []byte `gorm:"type:text;not null;column:data;comment:版本差异,包含api和upstream"` +} + +func (t *Diff) IdValue() int64 { + return t.Id +} +func (t *Diff) TableName() string { + return "service_publish_diff" +} + +type Latest struct { + Id int64 `gorm:"column:id;type:BIGINT(20);NOT NULL;comment:id;primary_key;comment:主键ID;"` + Release string `gorm:"type:varchar(36);not null;column:release;comment:release id;uniqueIndex:release"` + Latest string `gorm:"type:varchar(36);not null;column:latest;comment:latest id;"` +} + +func (t *Latest) IdValue() int64 { + return t.Id +} +func (t *Latest) TableName() string { + return "service_publish_latest" +} + +type Status struct { + Id int64 `gorm:"column:id;type:BIGINT(20);NOT NULL;comment:id;primary_key;comment:主键ID;"` + Publish string `gorm:"type:varchar(36);not null;column:publish;comment:publish id;uniqueIndex:unique"` + Cluster string `gorm:"type:varchar(36);not null;column:cluster;comment:cluster;uniqueIndex:unique"` + Status int `gorm:"type:int(11);not null;column:status;index:status; comment:状态, 0: 申请中, 1: 审批中, 2: 审批通过, 3: 审批拒绝, 4: 已发布 5: 已中止 6: 已关闭 7: 发布中 8:发布失败"` + Error string `gorm:"type:text;not null;column:error;comment:错误信息"` + UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:update_at;comment:更新时间"` +} + +func (t *Status) IdValue() int64 { + return t.Id +} +func (t *Status) TableName() string { + return "service_publish_status" +} diff --git a/stores/publish/store.go b/stores/publish/store.go new file mode 100644 index 00000000..9e5bc779 --- /dev/null +++ b/stores/publish/store.go @@ -0,0 +1,48 @@ +package publish + +import ( + "reflect" + + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/store" +) + +type IPublishStore interface { + store.IBaseStore[Publish] +} +type IDiffStore interface { + store.IBaseStore[Diff] +} +type IPublishLatestStore interface { + store.IBaseStore[Latest] +} + +type IPublishStatusStore interface { + store.IBaseStore[Status] +} + +var ( + _ IPublishStore = (*store.Store[Publish])(nil) + _ IDiffStore = (*store.Store[Diff])(nil) + _ IPublishLatestStore = (*store.Store[Latest])(nil) + _ IPublishStatusStore = (*store.Store[Status])(nil) +) + +func init() { + autowire.Auto[IPublishStore](func() reflect.Value { + return reflect.ValueOf(new(store.Store[Publish])) + }) + + autowire.Auto[IDiffStore](func() reflect.Value { + return reflect.ValueOf(new(store.Store[Diff])) + }) + + autowire.Auto[IPublishLatestStore](func() reflect.Value { + return reflect.ValueOf(new(store.Store[Latest])) + }) + + autowire.Auto[IPublishStatusStore](func() reflect.Value { + return reflect.ValueOf(new(store.Store[Status])) + }) + +} diff --git a/stores/release/release.go b/stores/release/release.go new file mode 100644 index 00000000..8dfadce1 --- /dev/null +++ b/stores/release/release.go @@ -0,0 +1,36 @@ +package release + +import "time" + +type Release struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Name string `gorm:"type:varchar(100);not null;column:name;comment:name"` + Service string `gorm:"type:varchar(50);not null;column:service;comment:服务ID;index:service"` + Remark string `gorm:"size:255;not null;column:remark;comment:备注"` + Creator string `gorm:"size:36;not null;column:creator;comment:创建人;index:creator"` // 创建人 + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` +} + +func (r *Release) IdValue() int64 { + return r.Id +} +func (r *Release) TableName() string { + return "release" +} + +type Commit struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + Release string `gorm:"type:varchar(36);not null;column:release;comment:release id; index:release; uniqueIndex:type_release_type_key"` + Type string `gorm:"type:varchar(10);not null;column:type;comment:类型;index:type;uniqueIndex:type_release_type_key"` + Target string `gorm:"type:varchar(36);not null;column:target;comment:目标;index:target;uniqueIndex:type_release_type_key"` + Key string `gorm:"type:varchar(36);not null;column:api;comment:api id;uniqueIndex:type_release_type_key"` + Commit string `gorm:"type:varchar(36);not null;column:commit;comment:commit;"` +} + +func (t *Commit) IdValue() int64 { + return t.Id +} +func (t *Commit) TableName() string { + return "release_commit" +} diff --git a/stores/release/runtime.go b/stores/release/runtime.go new file mode 100644 index 00000000..c0798859 --- /dev/null +++ b/stores/release/runtime.go @@ -0,0 +1,18 @@ +package release + +import "time" + +type Runtime struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + Service string `gorm:"type:varchar(50);not null;column:service;comment:服务ID;index:service"` + Release string `gorm:"type:varchar(36);not null;column:release;comment:release id;"` + UpdateTime time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:update_time;comment:更新时间"` + Operator string `gorm:"size:36;not null;column:operator;comment:操作人;index:operator"` +} + +func (t *Runtime) IdValue() int64 { + return t.Id +} +func (t *Runtime) TableName() string { + return "service_runtime" +} diff --git a/stores/release/store.go b/stores/release/store.go new file mode 100644 index 00000000..1f775c14 --- /dev/null +++ b/stores/release/store.go @@ -0,0 +1,38 @@ +package release + +import ( + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/store" + "reflect" +) + +var ( + _ IReleaseStore = (*store.Store[Release])(nil) + _ IReleaseCommitStore = (*store.Store[Commit])(nil) + _ IReleaseRuntime = (*store.Store[Runtime])(nil) +) + +type IReleaseStore interface { + store.IBaseStore[Release] +} +type IReleaseCommitStore interface { + store.IBaseStore[Commit] +} + +type IReleaseRuntime interface { + store.IBaseStore[Runtime] +} + +func init() { + autowire.Auto[IReleaseStore](func() reflect.Value { + return reflect.ValueOf(new(store.Store[Release])) + }) + + autowire.Auto[IReleaseCommitStore](func() reflect.Value { + return reflect.ValueOf(new(store.Store[Commit])) + }) + autowire.Auto[IReleaseRuntime](func() reflect.Value { + return reflect.ValueOf(new(store.Store[Runtime])) + }) + +} diff --git a/stores/server/model.go b/stores/server/model.go new file mode 100644 index 00000000..e9a9e428 --- /dev/null +++ b/stores/server/model.go @@ -0,0 +1,81 @@ +package server + +import "time" + +type ServerCommit struct { + Description string `json:"description,omitempty"` + Logo string `json:"logo,omitempty"` + Group string `json:"group,omitempty"` + GroupName string `json:"groupName,omitempty"` + Online bool `json:"online,omitempty"` +} + +type Server struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Name string `gorm:"type:varchar(100);not null;column:name;comment:name"` + + Project string `gorm:"size:36;not null;column:project;comment:项目名称"` + Team string `gorm:"size:36;not null;column:team;comment:团队id"` + Group string `gorm:"size:36;not null;column:group;comment:组id"` + Delete string `gorm:"size:36;not null;column:delete;comment:是否删除 0:未删除 1:已删除"` + Creator string `gorm:"size:36;not null;column:creator;comment:创建人;index:creator"` // 创建人 + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` +} + +func (s *Server) IdValue() int64 { + return s.Id +} +func (s *Server) TableName() string { + return "server" +} + +type Group struct { + Id int64 `gorm:"column:id;type:BIGINT(20);NOT NULL;comment:id;primary_key;comment:主键ID;"` + Group string `gorm:"size:36;not null;column:group;comment:组id"` +} + +func (g *Group) IdValue() int64 { + return g.Id +} +func (g *Group) TableName() string { + return "server_group" +} + +type Partition struct { + Id int64 `gorm:"type:BIGINT(20);size:20;not null;auto_increment;primary_key;column:id;comment:主键ID;"` + Sid string `gorm:"size:36;not null;column:sid;comment:服务id;uniqueIndex:sid_pid; index:sid;"` + Pid string `gorm:"size:36;not null;column:pid;comment:分区id;uniqueIndex:sid_pid;index:pid;"` +} + +func (p *Partition) IdValue() int64 { + return p.Id +} +func (p *Partition) TableName() string { + return "server_partition" +} + +type Online struct { + Id int64 `gorm:"type:BIGINT(20);size:20;not null;auto_increment;primary_key;column:id;comment:主键ID;"` + Sid string `gorm:"size:36;not null;column:sid;uniqueIndex:sid;comment:服务id;uniqueIndex:sid;"` +} + +func (o *Online) IdValue() int64 { + return o.Id +} +func (o *Online) TableName() string { + return "server_online" +} + +type Api struct { + Id int64 `gorm:"type:BIGINT(20);size:20;not null;auto_increment;primary_key;column:id;comment:主键ID;"` + Suid string `gorm:"size:36;not null;column:suid;uniqueIndex:suid_api;index:suid;comment:服务id;"` + Api string `gorm:"size:36;not null;column:api;uniqueIndex:sid_api;comment:api id;index:api;"` +} + +func (a *Api) IdValue() int64 { + return a.Id +} +func (a *Api) TableName() string { + return "server_api" +} diff --git a/stores/server/store.go b/stores/server/store.go new file mode 100644 index 00000000..92f7808b --- /dev/null +++ b/stores/server/store.go @@ -0,0 +1,51 @@ +package server + +import ( + "github.com/APIParkLab/APIPark/stores/universally/commit" + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/store" + "reflect" +) + +type IServerStore interface { + store.ISearchStore[Server] +} + +type imlServerStore struct { + store.SearchStore[Server] +} + +type IServerApiStore interface { + store.IBaseStore[Api] +} +type storeServerApi struct { + store.Store[Api] +} +type IServerCommit interface { + commit.ICommitWKStore[ServerCommit] +} +type IServicePartitionStore interface { + store.IBaseStore[Partition] +} + +type storeServerPartition struct { + store.Store[Partition] +} + +func init() { + autowire.Auto[IServerStore](func() reflect.Value { + return reflect.ValueOf(new(imlServerStore)) + }) + + autowire.Auto[IServerApiStore](func() reflect.Value { + return reflect.ValueOf(new(storeServerApi)) + }) + + autowire.Auto[IServerCommit](func() reflect.Value { + return reflect.ValueOf(commit.NewCommitWithKey[ServerCommit]("server", "setting")) + }) + autowire.Auto[IServicePartitionStore](func() reflect.Value { + return reflect.ValueOf(new(storeServerPartition)) + }) + +} diff --git a/stores/service/model.go b/stores/service/model.go new file mode 100644 index 00000000..49ddd4bc --- /dev/null +++ b/stores/service/model.go @@ -0,0 +1,85 @@ +package service + +import "time" + +type Service struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Name string `gorm:"type:varchar(100);not null;column:name;comment:name"` + + Description string `gorm:"size:255;not null;column:description;comment:description"` + Prefix string `gorm:"size:255;not null;column:prefix;comment:前缀"` + Team string `gorm:"size:36;not null;column:team;comment:团队id;index:team"` // 团队id + Logo string `gorm:"type:text;not null;column:logo;comment:logo"` + ServiceType int `gorm:"type:int(11);not null;column:service_type;comment:服务类型"` + Catalogue string `gorm:"type:text;not null;column:catalogue;comment:目录"` + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` + UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;column:update_at;comment:修改时间"` + IsDelete int `gorm:"type:tinyint(1);not null;column:is_delete;comment:是否删除"` + AsServer bool `gorm:"type:tinyint(1);not null;column:as_server;comment:是否为服务端项目"` + AsApp bool `gorm:"type:tinyint(1);not null;column:as_app;comment:是否为应用项目"` +} + +func (p *Service) IdValue() int64 { + return p.Id +} +func (p *Service) TableName() string { + return "service" +} + +type Authorization struct { + Id int64 `gorm:"type:BIGINT(20);size:20;not null;auto_increment;primary_key;column:id;comment:主键ID;"` + UUID string `gorm:"size:36;not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Name string `gorm:"size:100;not null;column:name;comment:名称"` + Application string `gorm:"size:100;not null;column:application;comment:应用"` + Type string `gorm:"size:100;not null;column:type;comment:类型"` + Position string `gorm:"size:100;not null;column:position;comment:位置"` + TokenName string `gorm:"size:100;not null;column:token_name;comment:token名称"` + Config string `gorm:"type:text;not null;column:config;comment:配置"` + Creator string `gorm:"size:36;not null;column:creator;comment:创建者" aovalue:"creator"` + Updater string `gorm:"size:36;not null;column:updater;comment:修改者" aovalue:"updater"` + ExpireTime int64 `gorm:"type:BIGINT(20);not null;column:expire_time;comment:过期时间"` + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` + UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;column:update_at;comment:修改时间"` + HideCredential bool `gorm:"type:tinyint(1);not null;column:hide_credential;comment:隐藏凭证"` +} + +func (a *Authorization) IdValue() int64 { + return a.Id +} + +func (a *Authorization) TableName() string { + return "service_authorization" +} + +type Tag struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + Tid string `gorm:"size:36;not null;column:tid;comment:标签id;uniqueIndex:sid_tid;index:tid;"` + Sid string `gorm:"size:36;not null;column:sid;comment:服务id;uniqueIndex:sid_tid;index:sid;"` +} + +func (t *Tag) IdValue() int64 { + return t.Id +} + +func (t *Tag) TableName() string { + return "server_tag" +} + +type Doc struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + Sid string `gorm:"size:36;not null;column:sid;comment:服务id;uniqueIndex:unique_sid;"` + Doc string `gorm:"type:text;column:content;comment:内容"` + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` + UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;column:update_at;comment:修改时间" json:"update_at"` + Creator string `gorm:"type:varchar(36);not null;column:creator;comment:创建者"` + Updater string `gorm:"type:varchar(36);not null;column:updater;comment:修改者"` +} + +func (d *Doc) IdValue() int64 { + return d.Id +} + +func (d *Doc) TableName() string { + return "server_doc" +} diff --git a/stores/service/store.go b/stores/service/store.go new file mode 100644 index 00000000..8c24fc15 --- /dev/null +++ b/stores/service/store.go @@ -0,0 +1,55 @@ +package service + +import ( + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/store" +) +import "reflect" + +type IServiceStore interface { + store.ISearchStore[Service] +} +type imlServiceStore struct { + store.SearchStore[Service] +} + +type IServiceTagStore interface { + store.IBaseStore[Tag] +} + +type imlServiceTagStore struct { + store.Store[Tag] +} + +type IServiceDocStore interface { + store.ISearchStore[Doc] +} + +type imlServiceDocStore struct { + store.SearchStore[Doc] +} + +type IAuthorizationStore interface { + store.ISearchStore[Authorization] +} + +type imlAuthorizationStore struct { + store.SearchStore[Authorization] +} + +func init() { + autowire.Auto[IServiceStore](func() reflect.Value { + return reflect.ValueOf(new(imlServiceStore)) + }) + autowire.Auto[IAuthorizationStore](func() reflect.Value { + return reflect.ValueOf(new(imlAuthorizationStore)) + }) + autowire.Auto[IServiceTagStore](func() reflect.Value { + return reflect.ValueOf(new(imlServiceTagStore)) + }) + + autowire.Auto[IServiceDocStore](func() reflect.Value { + return reflect.ValueOf(new(imlServiceDocStore)) + }) + +} diff --git a/stores/setting/model.go b/stores/setting/model.go new file mode 100644 index 00000000..14e23342 --- /dev/null +++ b/stores/setting/model.go @@ -0,0 +1,19 @@ +package setting + +import "time" + +type Setting struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + Name string `gorm:"size:255;not null;column:name;comment:name;uniqueIndex:name;"` + Value string `gorm:"type:text;not null;column:value;comment:value;"` + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` + UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;column:update_at;comment:修改时间"` + Operator string `gorm:"type:varchar(36);not null;column:operator;comment:操作人;"` +} + +func (s *Setting) IdValue() int64 { + return s.Id +} +func (s *Setting) TableName() string { + return "apinto_setting" +} diff --git a/stores/setting/store.go b/stores/setting/store.go new file mode 100644 index 00000000..3b8989c9 --- /dev/null +++ b/stores/setting/store.go @@ -0,0 +1,58 @@ +package setting + +import ( + "context" + "errors" + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/store" + "gorm.io/gorm" + "reflect" + "time" +) + +var ( + _ ISettingStore = (*imlSettingStore)(nil) +) + +type ISettingStore interface { + Get(ctx context.Context, name string) (*Setting, error) + Set(ctx context.Context, name string, value string, operator string) error +} +type imlSettingStore struct { + store.Store[Setting] +} + +func (i *imlSettingStore) Get(ctx context.Context, name string) (*Setting, error) { + return i.Store.First(ctx, map[string]interface{}{"name": name}) +} + +func (i *imlSettingStore) Set(ctx context.Context, name string, value string, operator string) error { + return i.Store.Transaction(ctx, func(ctx context.Context) error { + v, err := i.Store.First(ctx, map[string]interface{}{"name": name}) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + if v == nil { + v = &Setting{ + Name: name, + CreateAt: time.Now(), + UpdateAt: time.Now(), + Value: value, + Operator: operator, + } + return i.Store.Insert(ctx, v) + } + v.Value = value + v.Operator = operator + v.UpdateAt = time.Now() + _, err = i.Store.Update(ctx, v) + return err + + }) +} + +func init() { + autowire.Auto[ISettingStore](func() reflect.Value { + return reflect.ValueOf(new(imlSettingStore)) + }) +} diff --git a/stores/subscribe/model.go b/stores/subscribe/model.go new file mode 100644 index 00000000..7b8baa8b --- /dev/null +++ b/stores/subscribe/model.go @@ -0,0 +1,47 @@ +package subscribe + +import "time" + +type Subscribe struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"size:36;not null;column:uuid;comment:uuid;uniqueIndex:uuid;"` + Service string `gorm:"size:36;not null;column:service;comment:服务id;uniqueIndex:unique_subscribe"` + Application string `gorm:"size:36;not null;column:application;comment:应用id,项目id,系统id;uniqueIndex:unique_subscribe"` + ApplyStatus int `gorm:"type:tinyint(1);not null;column:apply_status;comment:申请状态;index:status;"` + Applier string `gorm:"size:36;not null;column:applier;comment:申请人;index:applier"` + From int `gorm:"type:tinyint(1);not null;column:from;comment:来源;index:status;"` + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` + ApproveAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:approve_at;comment:审批时间"` +} + +func (s *Subscribe) IdValue() int64 { + return s.Id +} + +func (s *Subscribe) TableName() string { + return "subscribe" +} + +type Apply struct { + Id int64 `gorm:"column:id;type:BIGINT(20);NOT NULL;comment:id;primary_key;comment:主键ID;"` + Uuid string `gorm:"size:36;not null;column:uuid;comment:uuid;uniqueIndex:uuid;"` // uuid + Service string `gorm:"size:36;not null;column:service;comment:服务id;index:service"` // 服务id + Team string `gorm:"size:36;not null;column:team;comment:团队id;index:team;"` // 团队id + Application string `gorm:"size:36;not null;column:application;comment:应用id,项目id,系统id;index:application"` // 订阅应用id + ApplyTeam string `gorm:"size:36;not null;column:apply_team;comment:申请团队id;index:apply_team;"` // 申请团队id + Applier string `gorm:"size:36;not null;column:applier;comment:申请人;index:applier;" aovalue:"creator"` + ApplyAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:apply_at;comment:申请时间"` + Approver string `gorm:"size:36;not null;column:approver;comment:审批人;index:approver;"` + ApproveAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:approve_at;comment:审批时间"` + Status int `gorm:"type:tinyint(1);not null;column:status;comment:审批状态;index:status;"` + Opinion string `gorm:"type:text;not null;column:opinion;comment:审批意见;"` + Reason string `gorm:"type:text;not null;column:reason;comment:申请原因;"` +} + +func (a *Apply) IdValue() int64 { + return a.Id +} + +func (a *Apply) TableName() string { + return "subscribe_apply" +} diff --git a/stores/subscribe/store.go b/stores/subscribe/store.go new file mode 100644 index 00000000..7895e9c4 --- /dev/null +++ b/stores/subscribe/store.go @@ -0,0 +1,34 @@ +package subscribe + +import ( + "reflect" + + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/store" +) + +type ISubscribeStore interface { + store.ISearchStore[Subscribe] +} + +type imlSubscribeStore struct { + store.SearchStore[Subscribe] +} + +type ISubscribeApplyStore interface { + store.ISearchStore[Apply] +} + +type imlSubscribeApplyStore struct { + store.SearchStore[Apply] +} + +func init() { + autowire.Auto[ISubscribeStore](func() reflect.Value { + return reflect.ValueOf(new(imlSubscribeStore)) + }) + + autowire.Auto[ISubscribeApplyStore](func() reflect.Value { + return reflect.ValueOf(new(imlSubscribeApplyStore)) + }) +} diff --git a/stores/tag/model.go b/stores/tag/model.go new file mode 100644 index 00000000..44845bbe --- /dev/null +++ b/stores/tag/model.go @@ -0,0 +1,19 @@ +package tag + +import "time" + +type Tag struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Name string `gorm:"type:varchar(100);not null;column:name;comment:name;uniqueIndex:name"` + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` + UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;column:update_at;comment:修改时间" json:"update_at"` +} + +func (o *Tag) TableName() string { + return "tag" +} + +func (o *Tag) IdValue() int64 { + return o.Id +} diff --git a/stores/tag/store.go b/stores/tag/store.go new file mode 100644 index 00000000..615d4458 --- /dev/null +++ b/stores/tag/store.go @@ -0,0 +1,22 @@ +package tag + +import ( + "reflect" + + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/store" +) + +type ITagStore interface { + store.ISearchStore[Tag] +} + +type imlTagStore struct { + store.SearchStore[Tag] +} + +func init() { + autowire.Auto[ITagStore](func() reflect.Value { + return reflect.ValueOf(new(imlTagStore)) + }) +} diff --git a/stores/team/model.go b/stores/team/model.go new file mode 100644 index 00000000..4ab5aba7 --- /dev/null +++ b/stores/team/model.go @@ -0,0 +1,38 @@ +package team + +import ( + "time" +) + +type Team struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Name string `gorm:"type:varchar(100);not null;column:name;comment:name"` + Description string `gorm:"size:255;not null;column:description;comment:description"` + Creator string `gorm:"size:36;not null;column:creator;comment:创建人id" aovalue:"creator"` // 创建人id + Updater string `gorm:"size:36;not null;column:updater;comment:修改人id" aovalue:"updater"` // 修改人id + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` + UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;column:update_at;comment:修改时间" json:"update_at"` + IsDelete bool `gorm:"type:tinyint(1);not null;column:is_delete;comment:是否删除"` +} + +func (t *Team) IdValue() int64 { + return t.Id +} +func (t *Team) TableName() string { + return "team" +} + +type Member struct { + Id int64 `gorm:"type:BIGINT(20);size:20;not null;auto_increment;primary_key;column:id;comment:主键ID;"` + Tid string `gorm:"size:36;not null;column:tid;uniqueIndex:tid;comment:团队id;uniqueIndex:tid_uid;"` + Uid string `gorm:"size:36;not null;column:uid;uniqueIndex:uid;comment:用户id;uniqueIndex:tid_uid;"` + CreateTime time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` +} + +func (m *Member) IdValue() int64 { + return m.Id +} +func (m *Member) TableName() string { + return "team_member" +} diff --git a/stores/team/store.go b/stores/team/store.go new file mode 100644 index 00000000..ac041c89 --- /dev/null +++ b/stores/team/store.go @@ -0,0 +1,29 @@ +package team + +import ( + "reflect" + + "github.com/eolinker/ap-account/store/member" + + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/store" +) + +type ITeamStore interface { + store.ISearchStore[Team] +} + +type ITeamMemberStore member.IMemberStore + +type imlTeamStore struct { + store.SearchStoreSoftDelete[Team] +} + +func init() { + autowire.Auto[ITeamStore](func() reflect.Value { + return reflect.ValueOf(new(imlTeamStore)) + }) + autowire.Auto[ITeamMemberStore](func() reflect.Value { + return reflect.ValueOf(member.NewMemberStore("team")) + }) +} diff --git a/stores/universally/attribute/model.go b/stores/universally/attribute/model.go new file mode 100644 index 00000000..f0332343 --- /dev/null +++ b/stores/universally/attribute/model.go @@ -0,0 +1,12 @@ +package attribute + +type Attribute struct { + Id int64 `gorm:"type:BIGINT(20);size:20;not null;auto_increment;primary_key;column:id;comment:主键ID;"` + Target int64 `gorm:"type:BIGINT(20);size:20;not null;column:target;comment:target id;index:tid; uniqueIndex:target_attribute;"` + Attribute string `gorm:"type:varchar(20);not null;column:attribute;comment:属性;uniqueIndex:target_attribute;"` + Value string `gorm:"type:varchar(255);not null;column:value;comment:属性值;"` +} + +func (t *Attribute) IdValue() int64 { + return t.Id +} diff --git a/stores/universally/commit/commit.go b/stores/universally/commit/commit.go new file mode 100644 index 00000000..898e9a0f --- /dev/null +++ b/stores/universally/commit/commit.go @@ -0,0 +1,19 @@ +package commit + +import "time" + +type Commit[H any] struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"size:36;not null;column:uuid;comment:uuid;uniqueIndex:uuid;"` + Target string `gorm:"column:target;type:varchar(36);NOT NULL;comment:目标id;index:target;"` + Key string `gorm:"size:36;not null;column:key;comment:类型;index:key;"` + Data *H `gorm:"type:text;not null;column:data;comment:数据;serializer:json"` + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` + Operator string `gorm:"size:36;not null;column:operator;comment:操作人;index:operator;"` +} +type Latest struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + Target string `gorm:"column:target;type:varchar(36);NOT NULL;comment:目标id;index:target;uniqueIndex:target_type;"` + Key string `gorm:"size:36;not null;column:key;comment:类型;index:key;uniqueIndex:target_type;"` + Latest string `gorm:"size:36;not null;column:latest;comment:最新版本 id"` +} diff --git a/stores/universally/commit/once.go b/stores/universally/commit/once.go new file mode 100644 index 00000000..2f0c9d4b --- /dev/null +++ b/stores/universally/commit/once.go @@ -0,0 +1,18 @@ +package commit + +import "sync" + +var ( + lock sync.Mutex + onceMap = make(map[string]struct{}) +) + +func onceMigrate(key string, f func()) { + lock.Lock() + defer lock.Unlock() + if _, ok := onceMap[key]; ok { + return + } + f() + onceMap[key] = struct{}{} +} diff --git a/stores/universally/commit/store.go b/stores/universally/commit/store.go new file mode 100644 index 00000000..bfd250d9 --- /dev/null +++ b/stores/universally/commit/store.go @@ -0,0 +1,168 @@ +package commit + +import ( + "context" + "errors" + "github.com/eolinker/go-common/utils" + "strings" + "time" + + "github.com/eolinker/go-common/store" + "github.com/google/uuid" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +var ( + _ ICommitStore[any] = (*Store[any])(nil) +) + +type ICommitStore[H any] interface { + Save(ctx context.Context, key string, target string, h *H) error + Latest(ctx context.Context, key string, target ...string) ([]*Commit[H], error) + Get(ctx context.Context, uuid string) (*Commit[H], error) + List(ctx context.Context, uuids ...string) ([]*Commit[H], error) +} + +type Store[H any] struct { + db store.IDB `autowired:""` + latestTableName string + commitTableName string + name string +} + +func (h *Store[H]) List(ctx context.Context, uuids ...string) ([]*Commit[H], error) { + if len(uuids) == 0 { + return nil, errors.New("uuids is empty") + } + db := h.db.DB(ctx).Table(h.commitTableName) + if len(uuids) == 1 { + db = db.Where("`uuid` = ?", uuids[0]) + } else { + db = db.Where("`uuid` in ?", uuids) + } + + var commit = make([]*Commit[H], 0, len(uuids)) + err := db.Find(&commit).Error + if err != nil { + return nil, err + } + return commit, nil +} + +func NewCommitStore[H any](name string) *Store[H] { + return &Store[H]{ + name: name, + latestTableName: name + "_latest", + commitTableName: name + "_commit", + } +} + +func (h *Store[H]) OnComplete() { + + onceMigrate(h.name, func() { + db := h.db.DB(context.Background()) + + err := db.Table(h.commitTableName).AutoMigrate(&Commit[H]{}) + if err != nil { + panic(err) + } + err = db.Table(h.latestTableName).AutoMigrate(&Latest{}) + if err != nil { + panic(err) + } + }) + +} + +var ( + latestUniques = []clause.Column{ + { + Name: "target", + }, { + Name: "type", + }, + } +) + +func (h *Store[H]) Save(ctx context.Context, key string, target string, commit *H) error { + hid := uuid.NewString() + ho := &Commit[H]{ + Id: 0, + UUID: hid, + Target: target, + Key: key, + Data: commit, + CreateAt: time.Now(), + } + lo := &Latest{ + Id: 0, + Target: target, + Key: key, + Latest: hid, + } + + return h.db.DB(ctx).Transaction(func(tx *gorm.DB) error { + err := tx.Table(h.commitTableName).Create(ho).Error + if err != nil { + return err + } + + return tx.Table(h.latestTableName).Clauses(clause.OnConflict{ + Columns: latestUniques, + UpdateAll: true, + }).Create(lo).Error + + }) +} +func (h *Store[H]) Latest(ctx context.Context, key string, target ...string) ([]*Commit[H], error) { + + wheres := make([]string, 0, 2) + args := make([]interface{}, 0, 2) + if key == "" && len(target) == 0 { + return nil, errors.New("key or target is required") + } + if key != "" { + wheres = append(wheres, "`key` = ?") + args = append(args, key) + } + if len(target) > 0 { + if len(target) > 1 { + wheres = append(wheres, "`target` in ?") + args = append(args, target) + } else { + wheres = append(wheres, "`target` = ?") + args = append(args, target[0]) + } + } + + latest := make([]*Latest, 0) + err := h.db.DB(ctx).Debug().Table(h.latestTableName).Where(strings.Join(wheres, " and "), args...).Find(&latest).Error + + if err != nil { + return nil, err + } + latestUUUID := utils.SliceToSlice(latest, func(l *Latest) string { return l.Latest }) + ho := make([]*Commit[H], 0, len(latest)) + if len(latestUUUID) == 0 { + return nil, nil + } + if len(latestUUUID) > 1 { + err = h.db.DB(ctx).Table(h.commitTableName).Where("`uuid` in ?", latestUUUID).Find(&ho).Error + + } else { + err = h.db.DB(ctx).Table(h.commitTableName).Where("`uuid` = ?", latestUUUID[0]).Find(&ho).Error + } + if err != nil { + return nil, err + } + return ho, nil +} +func (h *Store[H]) Get(ctx context.Context, uuid string) (*Commit[H], error) { + var commit = new(Commit[H]) + err := h.db.DB(ctx).Table(h.commitTableName).Where("`uuid` = ?", uuid).First(commit).Error + if err != nil { + return nil, err + } + return commit, nil +} diff --git a/stores/universally/commit/wtype.go b/stores/universally/commit/wtype.go new file mode 100644 index 00000000..ee9a967a --- /dev/null +++ b/stores/universally/commit/wtype.go @@ -0,0 +1,42 @@ +package commit + +import ( + "context" +) + +var ( + _ ICommitWKStore[any] = (*StoreWidthType[any])(nil) +) + +type ICommitWKStore[H any] interface { + Save(ctx context.Context, target string, h *H) error + Latest(ctx context.Context, target ...string) ([]*Commit[H], error) + Get(ctx context.Context, uuid string) (*Commit[H], error) + List(ctx context.Context, uuids ...string) ([]*Commit[H], error) +} +type StoreWidthType[H any] struct { + Store[H] + key string +} + +func NewCommitWithKey[H any](name, key string) *StoreWidthType[H] { + return &StoreWidthType[H]{ + Store: Store[H]{ + latestTableName: name + "_latest", + commitTableName: name + "_commit", + }, + key: key, + } +} +func (h *StoreWidthType[H]) List(ctx context.Context, uuids ...string) ([]*Commit[H], error) { + return h.Store.List(ctx, uuids...) +} +func (h *StoreWidthType[H]) Save(ctx context.Context, target string, commit *H) error { + return h.Store.Save(ctx, h.key, target, commit) +} +func (h *StoreWidthType[H]) Latest(ctx context.Context, target ...string) ([]*Commit[H], error) { + return h.Store.Latest(ctx, h.key, target...) +} +func (h *StoreWidthType[H]) Get(ctx context.Context, uuid string) (*Commit[H], error) { + return h.Store.Get(ctx, uuid) +} diff --git a/stores/upstream/model.go b/stores/upstream/model.go new file mode 100644 index 00000000..19383c72 --- /dev/null +++ b/stores/upstream/model.go @@ -0,0 +1,26 @@ +package upstream + +import ( + "time" +) + +type Upstream struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + UUID string `gorm:"type:varchar(36);not null;column:uuid;uniqueIndex:uuid;comment:UUID;"` + Name string `gorm:"size:255;not null;column:name;comment:名称"` + Project string `gorm:"size:36;not null;column:project;comment:项目;index:project;"` // 项目id + Team string `gorm:"size:36;not null;column:team;comment:团队;index:team;"` // 团队id + Remark string `gorm:"size:255;not null;column:remark;comment:备注"` + Creator string `gorm:"size:36;not null;column:creator;comment:创建人;index:creator;"` // 创建人 + Updater string `gorm:"size:36;not null;column:updater;comment:更新人;index:updater;"` // 更新人 + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` + UpdateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;column:update_at;comment:更新时间"` +} + +func (u *Upstream) IdValue() int64 { + return u.Id +} + +func (u *Upstream) TableName() string { + return "upstream" +} diff --git a/stores/upstream/store.go b/stores/upstream/store.go new file mode 100644 index 00000000..31d33d59 --- /dev/null +++ b/stores/upstream/store.go @@ -0,0 +1,22 @@ +package upstream + +import ( + "reflect" + + "github.com/eolinker/go-common/autowire" + "github.com/eolinker/go-common/store" +) + +type IUpstreamStore interface { + store.IBaseStore[Upstream] +} + +type storeUpstream struct { + store.Store[Upstream] +} + +func init() { + autowire.Auto[IUpstreamStore](func() reflect.Value { + return reflect.ValueOf(new(storeUpstream)) + }) +} diff --git a/stores/version/model.go b/stores/version/model.go new file mode 100644 index 00000000..c02c3450 --- /dev/null +++ b/stores/version/model.go @@ -0,0 +1,20 @@ +package version + +import ( + "time" +) + +type Version struct { + Id int64 `gorm:"column:id;type:BIGINT(20);AUTO_INCREMENT;NOT NULL;comment:id;primary_key;comment:主键ID;"` + + Version string `gorm:"size:36;not null;column:version;uniqueIndex:Version;comment:Version;"` + Description string `gorm:"size:255;not null;column:description;comment:description;"` + CreateAt time.Time `gorm:"type:timestamp;NOT NULL;DEFAULT:CURRENT_TIMESTAMP;column:create_at;comment:创建时间"` +} + +func (v *Version) IdValue() int64 { + return v.Id +} +func (v *Version) TableName() string { + return "version" +} diff --git a/stores/version/store.go b/stores/version/store.go new file mode 100644 index 00000000..f37d99d0 --- /dev/null +++ b/stores/version/store.go @@ -0,0 +1 @@ +package version