mirror of
https://github.com/casdoor/casdoor.git
synced 2025-08-17 13:04:45 +08:00
Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
953be4a7b6 | ||
![]() |
943cc43427 | ||
![]() |
1e5ce7a045 | ||
![]() |
7a85b74573 | ||
![]() |
7e349c1768 | ||
![]() |
b19be2df88 | ||
![]() |
fc3866db1c | ||
![]() |
bf2bb31e41 | ||
![]() |
ec8bd6f01d | ||
![]() |
98722fd681 | ||
![]() |
221c55aa93 | ||
![]() |
988b26b3c2 | ||
![]() |
7e3c361ce7 | ||
![]() |
a637707e77 | ||
![]() |
7970edeaa7 | ||
![]() |
9da2f0775f | ||
![]() |
739a9bcd0d | ||
![]() |
fb0949b9ed | ||
![]() |
27ed901167 | ||
![]() |
ceab662b88 |
@@ -140,13 +140,6 @@ func (c *ApiController) Signup() {
|
|||||||
username = id
|
username = id
|
||||||
}
|
}
|
||||||
|
|
||||||
password := authForm.Password
|
|
||||||
msg = object.CheckPasswordComplexityByOrg(organization, password)
|
|
||||||
if msg != "" {
|
|
||||||
c.ResponseError(msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
initScore, err := organization.GetInitScore()
|
initScore, err := organization.GetInitScore()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(fmt.Errorf(c.T("account:Get init score failed, error: %w"), err).Error())
|
c.ResponseError(fmt.Errorf(c.T("account:Get init score failed, error: %w"), err).Error())
|
||||||
|
@@ -61,6 +61,10 @@ func (c *ApiController) IsAdminOrSelf(user2 *object.User) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if user == nil || user2 == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if user.Owner == user2.Owner && user.Name == user2.Name {
|
if user.Owner == user2.Owner && user.Name == user2.Name {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@@ -114,7 +114,7 @@ func (c *ApiController) GetPlan() {
|
|||||||
// @router /update-plan [post]
|
// @router /update-plan [post]
|
||||||
func (c *ApiController) UpdatePlan() {
|
func (c *ApiController) UpdatePlan() {
|
||||||
id := c.Input().Get("id")
|
id := c.Input().Get("id")
|
||||||
|
owner := util.GetOwnerFromId(id)
|
||||||
var plan object.Plan
|
var plan object.Plan
|
||||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &plan)
|
err := json.Unmarshal(c.Ctx.Input.RequestBody, &plan)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -122,19 +122,21 @@ func (c *ApiController) UpdatePlan() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if plan.Product != "" {
|
if plan.Product != "" {
|
||||||
planId := util.GetId(plan.Owner, plan.Product)
|
productId := util.GetId(owner, plan.Product)
|
||||||
product, err := object.GetProduct(planId)
|
product, err := object.GetProduct(productId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if product != nil {
|
||||||
object.UpdateProductForPlan(&plan, product)
|
object.UpdateProductForPlan(&plan, product)
|
||||||
_, err = object.UpdateProduct(planId, product)
|
_, err = object.UpdateProduct(productId, product)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
c.Data["json"] = wrapActionResponse(object.UpdatePlan(id, &plan))
|
c.Data["json"] = wrapActionResponse(object.UpdatePlan(id, &plan))
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
@@ -160,7 +160,11 @@ func (c *ApiController) RunSyncer() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
object.RunSyncer(syncer)
|
err = object.RunSyncer(syncer)
|
||||||
|
if err != nil {
|
||||||
|
c.ResponseError(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
c.ResponseOk()
|
c.ResponseOk()
|
||||||
}
|
}
|
||||||
|
@@ -47,19 +47,16 @@ func (c *ApiController) GetSystemInfo() {
|
|||||||
// @router /get-version-info [get]
|
// @router /get-version-info [get]
|
||||||
func (c *ApiController) GetVersionInfo() {
|
func (c *ApiController) GetVersionInfo() {
|
||||||
versionInfo, err := util.GetVersionInfo()
|
versionInfo, err := util.GetVersionInfo()
|
||||||
if err != nil {
|
if versionInfo.Version != "" {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseOk(versionInfo)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if versionInfo.Version == "" {
|
|
||||||
versionInfo, err = util.GetVersionInfoFromFile()
|
versionInfo, err = util.GetVersionInfoFromFile()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.ResponseError(err.Error())
|
c.ResponseError(err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
c.ResponseOk(versionInfo)
|
c.ResponseOk(versionInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -66,8 +66,11 @@ func CheckUserSignup(application *Application, organization *Organization, form
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(form.Password) <= 5 {
|
if application.IsSignupItemVisible("Password") {
|
||||||
return i18n.Translate(lang, "check:Password must have at least 6 characters")
|
msg := CheckPasswordComplexityByOrg(organization, form.Password)
|
||||||
|
if msg != "" {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if application.IsSignupItemVisible("Email") {
|
if application.IsSignupItemVisible("Email") {
|
||||||
@@ -126,7 +129,9 @@ func CheckUserSignup(application *Application, organization *Organization, form
|
|||||||
|
|
||||||
if len(application.InvitationCodes) > 0 {
|
if len(application.InvitationCodes) > 0 {
|
||||||
if form.InvitationCode == "" {
|
if form.InvitationCode == "" {
|
||||||
|
if application.IsSignupItemRequired("Invitation code") {
|
||||||
return i18n.Translate(lang, "check:Invitation code cannot be blank")
|
return i18n.Translate(lang, "check:Invitation code cannot be blank")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if !util.InSlice(application.InvitationCodes, form.InvitationCode) {
|
if !util.InSlice(application.InvitationCodes, form.InvitationCode) {
|
||||||
return i18n.Translate(lang, "check:Invitation code is invalid")
|
return i18n.Translate(lang, "check:Invitation code is invalid")
|
||||||
|
@@ -16,6 +16,7 @@ package object
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/casdoor/casdoor/util"
|
"github.com/casdoor/casdoor/util"
|
||||||
"github.com/xorm-io/core"
|
"github.com/xorm-io/core"
|
||||||
@@ -28,10 +29,10 @@ type Plan struct {
|
|||||||
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
DisplayName string `xorm:"varchar(100)" json:"displayName"`
|
||||||
Description string `xorm:"varchar(100)" json:"description"`
|
Description string `xorm:"varchar(100)" json:"description"`
|
||||||
|
|
||||||
PricePerMonth float64 `json:"pricePerMonth"`
|
Price float64 `json:"price"`
|
||||||
PricePerYear float64 `json:"pricePerYear"`
|
|
||||||
Currency string `xorm:"varchar(100)" json:"currency"`
|
Currency string `xorm:"varchar(100)" json:"currency"`
|
||||||
Product string `json:"product"` // related product id
|
Period string `xorm:"varchar(100)" json:"period"`
|
||||||
|
Product string `xorm:"varchar(100)" json:"product"`
|
||||||
PaymentProviders []string `xorm:"varchar(100)" json:"paymentProviders"` // payment providers for related product
|
PaymentProviders []string `xorm:"varchar(100)" json:"paymentProviders"` // payment providers for related product
|
||||||
IsEnabled bool `json:"isEnabled"`
|
IsEnabled bool `json:"isEnabled"`
|
||||||
|
|
||||||
@@ -39,10 +40,28 @@ type Plan struct {
|
|||||||
Options []string `xorm:"-" json:"options"`
|
Options []string `xorm:"-" json:"options"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
PeriodMonthly = "Monthly"
|
||||||
|
PeriodYearly = "Yearly"
|
||||||
|
)
|
||||||
|
|
||||||
func (plan *Plan) GetId() string {
|
func (plan *Plan) GetId() string {
|
||||||
return fmt.Sprintf("%s/%s", plan.Owner, plan.Name)
|
return fmt.Sprintf("%s/%s", plan.Owner, plan.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetDuration(period string) (startTime time.Time, endTime time.Time) {
|
||||||
|
if period == PeriodYearly {
|
||||||
|
startTime = time.Now()
|
||||||
|
endTime = startTime.AddDate(1, 0, 0)
|
||||||
|
} else if period == PeriodMonthly {
|
||||||
|
startTime = time.Now()
|
||||||
|
endTime = startTime.AddDate(0, 1, 0)
|
||||||
|
} else {
|
||||||
|
panic(fmt.Sprintf("invalid period: %s", period))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func GetPlanCount(owner, field, value string) (int64, error) {
|
func GetPlanCount(owner, field, value string) (int64, error) {
|
||||||
session := GetSession(owner, -1, -1, field, value, "", "")
|
session := GetSession(owner, -1, -1, field, value, "", "")
|
||||||
return session.Count(&Plan{})
|
return session.Count(&Plan{})
|
||||||
|
@@ -32,12 +32,6 @@ type Pricing struct {
|
|||||||
IsEnabled bool `json:"isEnabled"`
|
IsEnabled bool `json:"isEnabled"`
|
||||||
TrialDuration int `json:"trialDuration"`
|
TrialDuration int `json:"trialDuration"`
|
||||||
Application string `xorm:"varchar(100)" json:"application"`
|
Application string `xorm:"varchar(100)" json:"application"`
|
||||||
|
|
||||||
Submitter string `xorm:"varchar(100)" json:"submitter"`
|
|
||||||
Approver string `xorm:"varchar(100)" json:"approver"`
|
|
||||||
ApproveTime string `xorm:"varchar(100)" json:"approveTime"`
|
|
||||||
|
|
||||||
State string `xorm:"varchar(100)" json:"state"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pricing *Pricing) GetId() string {
|
func (pricing *Pricing) GetId() string {
|
||||||
|
@@ -190,8 +190,15 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
|
|||||||
if user.Type == "paid-user" {
|
if user.Type == "paid-user" {
|
||||||
// Create a subscription for `paid-user`
|
// Create a subscription for `paid-user`
|
||||||
if pricingName != "" && planName != "" {
|
if pricingName != "" && planName != "" {
|
||||||
sub := NewSubscription(owner, user.Name, pricingName, planName, paymentName)
|
plan, err := GetPlan(util.GetId(owner, planName))
|
||||||
_, err := AddSubscription(sub)
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if plan == nil {
|
||||||
|
return "", "", fmt.Errorf("the plan: %s does not exist", planName)
|
||||||
|
}
|
||||||
|
sub := NewSubscription(owner, user.Name, plan.Name, paymentName, plan.Period)
|
||||||
|
_, err = AddSubscription(sub)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
@@ -267,14 +274,14 @@ func CreateProductForPlan(plan *Plan) *Product {
|
|||||||
product := &Product{
|
product := &Product{
|
||||||
Owner: plan.Owner,
|
Owner: plan.Owner,
|
||||||
Name: fmt.Sprintf("product_%v", util.GetRandomName()),
|
Name: fmt.Sprintf("product_%v", util.GetRandomName()),
|
||||||
DisplayName: fmt.Sprintf("Auto Created Product for Plan %v(%v)", plan.GetId(), plan.DisplayName),
|
DisplayName: fmt.Sprintf("Product for Plan %v/%v/%v", plan.Name, plan.DisplayName, plan.Period),
|
||||||
CreatedTime: plan.CreatedTime,
|
CreatedTime: plan.CreatedTime,
|
||||||
|
|
||||||
Image: "https://cdn.casbin.org/img/casdoor-logo_1185x256.png", // TODO
|
Image: "https://cdn.casbin.org/img/casdoor-logo_1185x256.png", // TODO
|
||||||
Detail: fmt.Sprintf("This Product was auto created for Plan %v(%v)", plan.GetId(), plan.DisplayName),
|
Detail: fmt.Sprintf("This product was auto created for plan %v(%v), subscription period is %v", plan.Name, plan.DisplayName, plan.Period),
|
||||||
Description: plan.Description,
|
Description: plan.Description,
|
||||||
Tag: "auto_created_product_for_plan",
|
Tag: "auto_created_product_for_plan",
|
||||||
Price: plan.PricePerMonth, // TODO
|
Price: plan.Price,
|
||||||
Currency: plan.Currency,
|
Currency: plan.Currency,
|
||||||
|
|
||||||
Quantity: 999,
|
Quantity: 999,
|
||||||
@@ -283,13 +290,17 @@ func CreateProductForPlan(plan *Plan) *Product {
|
|||||||
Providers: plan.PaymentProviders,
|
Providers: plan.PaymentProviders,
|
||||||
State: "Published",
|
State: "Published",
|
||||||
}
|
}
|
||||||
|
if product.Providers == nil {
|
||||||
|
product.Providers = []string{}
|
||||||
|
}
|
||||||
return product
|
return product
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateProductForPlan(plan *Plan, product *Product) {
|
func UpdateProductForPlan(plan *Plan, product *Product) {
|
||||||
product.DisplayName = fmt.Sprintf("Auto Created Product for Plan %v(%v)", plan.GetId(), plan.DisplayName)
|
product.Owner = plan.Owner
|
||||||
product.Detail = fmt.Sprintf("This Product was auto created for Plan %v(%v)", plan.GetId(), plan.DisplayName)
|
product.DisplayName = fmt.Sprintf("Product for Plan %v/%v/%v", plan.Name, plan.DisplayName, plan.Period)
|
||||||
product.Price = plan.PricePerMonth // TODO
|
product.Detail = fmt.Sprintf("This product was auto created for plan %v(%v), subscription period is %v", plan.Name, plan.DisplayName, plan.Period)
|
||||||
product.Providers = plan.PaymentProviders
|
product.Price = plan.Price
|
||||||
product.Currency = plan.Currency
|
product.Currency = plan.Currency
|
||||||
|
product.Providers = plan.PaymentProviders
|
||||||
}
|
}
|
||||||
|
@@ -50,7 +50,7 @@ type Subscription struct {
|
|||||||
|
|
||||||
StartTime time.Time `json:"startTime"`
|
StartTime time.Time `json:"startTime"`
|
||||||
EndTime time.Time `json:"endTime"`
|
EndTime time.Time `json:"endTime"`
|
||||||
Duration int `json:"duration"`
|
Period string `xorm:"varchar(100)" json:"period"`
|
||||||
State SubscriptionState `xorm:"varchar(100)" json:"state"`
|
State SubscriptionState `xorm:"varchar(100)" json:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +103,8 @@ func (sub *Subscription) UpdateState() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSubscription(owner, userName, pricingName, planName, paymentName string) *Subscription {
|
func NewSubscription(owner, userName, planName, paymentName, period string) *Subscription {
|
||||||
|
startTime, endTime := GetDuration(period)
|
||||||
id := util.GenerateId()[:6]
|
id := util.GenerateId()[:6]
|
||||||
return &Subscription{
|
return &Subscription{
|
||||||
Owner: owner,
|
Owner: owner,
|
||||||
@@ -112,13 +113,12 @@ func NewSubscription(owner, userName, pricingName, planName, paymentName string)
|
|||||||
CreatedTime: util.GetCurrentTime(),
|
CreatedTime: util.GetCurrentTime(),
|
||||||
|
|
||||||
User: userName,
|
User: userName,
|
||||||
Pricing: pricingName,
|
|
||||||
Plan: planName,
|
Plan: planName,
|
||||||
Payment: paymentName,
|
Payment: paymentName,
|
||||||
|
|
||||||
StartTime: time.Now(),
|
StartTime: startTime,
|
||||||
EndTime: time.Now().AddDate(0, 0, 30),
|
EndTime: endTime,
|
||||||
Duration: 30, // TODO
|
Period: period,
|
||||||
State: SubStatePending, // waiting for payment complete
|
State: SubStatePending, // waiting for payment complete
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -37,12 +37,13 @@ type Syncer struct {
|
|||||||
|
|
||||||
Organization string `xorm:"varchar(100)" json:"organization"`
|
Organization string `xorm:"varchar(100)" json:"organization"`
|
||||||
Type string `xorm:"varchar(100)" json:"type"`
|
Type string `xorm:"varchar(100)" json:"type"`
|
||||||
|
DatabaseType string `xorm:"varchar(100)" json:"databaseType"`
|
||||||
|
SslMode string `xorm:"varchar(100)" json:"sslMode"`
|
||||||
|
|
||||||
Host string `xorm:"varchar(100)" json:"host"`
|
Host string `xorm:"varchar(100)" json:"host"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
User string `xorm:"varchar(100)" json:"user"`
|
User string `xorm:"varchar(100)" json:"user"`
|
||||||
Password string `xorm:"varchar(100)" json:"password"`
|
Password string `xorm:"varchar(100)" json:"password"`
|
||||||
DatabaseType string `xorm:"varchar(100)" json:"databaseType"`
|
|
||||||
Database string `xorm:"varchar(100)" json:"database"`
|
Database string `xorm:"varchar(100)" json:"database"`
|
||||||
Table string `xorm:"varchar(100)" json:"table"`
|
Table string `xorm:"varchar(100)" json:"table"`
|
||||||
TableColumns []*TableColumn `xorm:"mediumtext" json:"tableColumns"`
|
TableColumns []*TableColumn `xorm:"mediumtext" json:"tableColumns"`
|
||||||
@@ -250,7 +251,7 @@ func (syncer *Syncer) getKey() string {
|
|||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunSyncer(syncer *Syncer) {
|
func RunSyncer(syncer *Syncer) error {
|
||||||
syncer.initAdapter()
|
syncer.initAdapter()
|
||||||
syncer.syncUsers()
|
return syncer.syncUsers()
|
||||||
}
|
}
|
||||||
|
@@ -52,11 +52,14 @@ func addSyncerJob(syncer *Syncer) error {
|
|||||||
|
|
||||||
syncer.initAdapter()
|
syncer.initAdapter()
|
||||||
|
|
||||||
syncer.syncUsers()
|
err := syncer.syncUsers()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
schedule := fmt.Sprintf("@every %ds", syncer.SyncInterval)
|
schedule := fmt.Sprintf("@every %ds", syncer.SyncInterval)
|
||||||
cron := getCronMap(syncer.Name)
|
cron := getCronMap(syncer.Name)
|
||||||
_, err := cron.AddFunc(schedule, syncer.syncUsers)
|
_, err = cron.AddFunc(schedule, syncer.syncUsersNoError)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@@ -19,15 +19,15 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (syncer *Syncer) syncUsers() {
|
func (syncer *Syncer) syncUsers() error {
|
||||||
if len(syncer.TableColumns) == 0 {
|
if len(syncer.TableColumns) == 0 {
|
||||||
return
|
return fmt.Errorf("The syncer table columns should not be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Running syncUsers()..\n")
|
fmt.Printf("Running syncUsers()..\n")
|
||||||
|
|
||||||
users, _, _ := syncer.getUserMap()
|
users, _, _ := syncer.getUserMap()
|
||||||
oUsers, oUserMap, err := syncer.getOriginalUserMap()
|
oUsers, _, err := syncer.getOriginalUserMap()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf(err.Error())
|
fmt.Printf(err.Error())
|
||||||
|
|
||||||
@@ -35,10 +35,8 @@ func (syncer *Syncer) syncUsers() {
|
|||||||
line := fmt.Sprintf("[%s] %s\n", timestamp, err.Error())
|
line := fmt.Sprintf("[%s] %s\n", timestamp, err.Error())
|
||||||
_, err = updateSyncerErrorText(syncer, line)
|
_, err = updateSyncerErrorText(syncer, line)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Users: %d, oUsers: %d\n", len(users), len(oUsers))
|
fmt.Printf("Users: %d, oUsers: %d\n", len(users), len(oUsers))
|
||||||
@@ -55,6 +53,11 @@ func (syncer *Syncer) syncUsers() {
|
|||||||
myUsers[syncer.getUserValue(m, key)] = m
|
myUsers[syncer.getUserValue(m, key)] = m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
myOUsers := map[string]*User{}
|
||||||
|
for _, m := range oUsers {
|
||||||
|
myOUsers[syncer.getUserValue(m, key)] = m
|
||||||
|
}
|
||||||
|
|
||||||
newUsers := []*User{}
|
newUsers := []*User{}
|
||||||
for _, oUser := range oUsers {
|
for _, oUser := range oUsers {
|
||||||
primary := syncer.getUserValue(oUser, key)
|
primary := syncer.getUserValue(oUser, key)
|
||||||
@@ -71,28 +74,30 @@ func (syncer *Syncer) syncUsers() {
|
|||||||
updatedUser := syncer.createUserFromOriginalUser(oUser, affiliationMap)
|
updatedUser := syncer.createUserFromOriginalUser(oUser, affiliationMap)
|
||||||
updatedUser.Hash = oHash
|
updatedUser.Hash = oHash
|
||||||
updatedUser.PreHash = oHash
|
updatedUser.PreHash = oHash
|
||||||
|
|
||||||
|
fmt.Printf("Update from oUser to user: %v\n", updatedUser)
|
||||||
_, err = syncer.updateUserForOriginalByFields(updatedUser, key)
|
_, err = syncer.updateUserForOriginalByFields(updatedUser, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf("Update from oUser to user: %v\n", updatedUser)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if user.PreHash == oHash {
|
if user.PreHash == oHash {
|
||||||
if !syncer.IsReadOnly {
|
if !syncer.IsReadOnly {
|
||||||
updatedOUser := syncer.createOriginalUserFromUser(user)
|
updatedOUser := syncer.createOriginalUserFromUser(user)
|
||||||
|
|
||||||
|
fmt.Printf("Update from user to oUser: %v\n", updatedOUser)
|
||||||
_, err = syncer.updateUser(updatedOUser)
|
_, err = syncer.updateUser(updatedOUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf("Update from user to oUser: %v\n", updatedOUser)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// update preHash
|
// update preHash
|
||||||
user.PreHash = user.Hash
|
user.PreHash = user.Hash
|
||||||
_, err = SetUserField(user, "pre_hash", user.PreHash)
|
_, err = SetUserField(user, "pre_hash", user.PreHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if user.Hash == oHash {
|
if user.Hash == oHash {
|
||||||
@@ -100,17 +105,18 @@ func (syncer *Syncer) syncUsers() {
|
|||||||
user.PreHash = user.Hash
|
user.PreHash = user.Hash
|
||||||
_, err = SetUserField(user, "pre_hash", user.PreHash)
|
_, err = SetUserField(user, "pre_hash", user.PreHash)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updatedUser := syncer.createUserFromOriginalUser(oUser, affiliationMap)
|
updatedUser := syncer.createUserFromOriginalUser(oUser, affiliationMap)
|
||||||
updatedUser.Hash = oHash
|
updatedUser.Hash = oHash
|
||||||
updatedUser.PreHash = oHash
|
updatedUser.PreHash = oHash
|
||||||
|
|
||||||
|
fmt.Printf("Update from oUser to user (2nd condition): %v\n", updatedUser)
|
||||||
_, err = syncer.updateUserForOriginalByFields(updatedUser, key)
|
_, err = syncer.updateUserForOriginalByFields(updatedUser, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf("Update from oUser to user (2nd condition): %v\n", updatedUser)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,20 +124,30 @@ func (syncer *Syncer) syncUsers() {
|
|||||||
}
|
}
|
||||||
_, err = AddUsersInBatch(newUsers)
|
_, err = AddUsersInBatch(newUsers)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !syncer.IsReadOnly {
|
if !syncer.IsReadOnly {
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
id := user.Id
|
primary := syncer.getUserValue(user, key)
|
||||||
if _, ok := oUserMap[id]; !ok {
|
if _, ok := myOUsers[primary]; !ok {
|
||||||
newOUser := syncer.createOriginalUserFromUser(user)
|
newOUser := syncer.createOriginalUserFromUser(user)
|
||||||
|
|
||||||
|
fmt.Printf("New oUser: %v\n", newOUser)
|
||||||
_, err = syncer.addUser(newOUser)
|
_, err = syncer.addUser(newOUser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
|
||||||
fmt.Printf("New oUser: %v\n", newOUser)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (syncer *Syncer) syncUsersNoError() {
|
||||||
|
err := syncer.syncUsers()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("syncUsersNoError() error: %s\n", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -31,8 +31,8 @@ type Credential struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (syncer *Syncer) getOriginalUsers() ([]*OriginalUser, error) {
|
func (syncer *Syncer) getOriginalUsers() ([]*OriginalUser, error) {
|
||||||
sql := fmt.Sprintf("select * from %s", syncer.getTable())
|
var results []map[string]string
|
||||||
results, err := syncer.Ormer.Engine.QueryString(sql)
|
err := syncer.Ormer.Engine.Table(syncer.getTable()).Find(&results)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -64,19 +64,10 @@ func (syncer *Syncer) getOriginalUserMap() ([]*OriginalUser, map[string]*Origina
|
|||||||
|
|
||||||
func (syncer *Syncer) addUser(user *OriginalUser) (bool, error) {
|
func (syncer *Syncer) addUser(user *OriginalUser) (bool, error) {
|
||||||
m := syncer.getMapFromOriginalUser(user)
|
m := syncer.getMapFromOriginalUser(user)
|
||||||
keyString, valueString := syncer.getSqlKeyValueStringFromMap(m)
|
affected, err := syncer.Ormer.Engine.Table(syncer.getTable()).Insert(m)
|
||||||
|
|
||||||
sql := fmt.Sprintf("insert into %s (%s) values (%s)", syncer.getTable(), keyString, valueString)
|
|
||||||
res, err := syncer.Ormer.Engine.Exec(sql)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
affected, err := res.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return affected != 0, nil
|
return affected != 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,23 +84,14 @@ func (syncer *Syncer) getCasdoorColumns() []string {
|
|||||||
|
|
||||||
func (syncer *Syncer) updateUser(user *OriginalUser) (bool, error) {
|
func (syncer *Syncer) updateUser(user *OriginalUser) (bool, error) {
|
||||||
key := syncer.getKey()
|
key := syncer.getKey()
|
||||||
|
|
||||||
m := syncer.getMapFromOriginalUser(user)
|
m := syncer.getMapFromOriginalUser(user)
|
||||||
pkValue := m[key]
|
pkValue := m[key]
|
||||||
delete(m, key)
|
delete(m, key)
|
||||||
setString := syncer.getSqlSetStringFromMap(m)
|
|
||||||
|
|
||||||
sql := fmt.Sprintf("update %s set %s where %s = %s", syncer.getTable(), setString, key, pkValue)
|
affected, err := syncer.Ormer.Engine.Table(syncer.getTable()).ID(pkValue).Update(&m)
|
||||||
res, err := syncer.Ormer.Engine.Exec(sql)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
affected, err := res.RowsAffected()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return affected != 0, nil
|
return affected != 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +167,11 @@ func (syncer *Syncer) initAdapter() {
|
|||||||
if syncer.DatabaseType == "mssql" {
|
if syncer.DatabaseType == "mssql" {
|
||||||
dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database)
|
dataSourceName = fmt.Sprintf("sqlserver://%s:%s@%s:%d?database=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database)
|
||||||
} else if syncer.DatabaseType == "postgres" {
|
} else if syncer.DatabaseType == "postgres" {
|
||||||
dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=disable dbname=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, syncer.Database)
|
sslMode := "disable"
|
||||||
|
if syncer.SslMode != "" {
|
||||||
|
sslMode = syncer.SslMode
|
||||||
|
}
|
||||||
|
dataSourceName = fmt.Sprintf("user=%s password=%s host=%s port=%d sslmode=%s dbname=%s", syncer.User, syncer.Password, syncer.Host, syncer.Port, sslMode, syncer.Database)
|
||||||
} else {
|
} else {
|
||||||
dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", syncer.User, syncer.Password, syncer.Host, syncer.Port)
|
dataSourceName = fmt.Sprintf("%s:%s@tcp(%s:%d)/", syncer.User, syncer.Password, syncer.Host, syncer.Port)
|
||||||
}
|
}
|
||||||
|
@@ -322,19 +322,3 @@ func (syncer *Syncer) getSqlSetStringFromMap(m map[string]string) string {
|
|||||||
}
|
}
|
||||||
return strings.Join(tokens, ", ")
|
return strings.Join(tokens, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (syncer *Syncer) getSqlKeyValueStringFromMap(m map[string]string) (string, string) {
|
|
||||||
typeMap := syncer.getTableColumnsTypeMap()
|
|
||||||
|
|
||||||
keys := []string{}
|
|
||||||
values := []string{}
|
|
||||||
for k, v := range m {
|
|
||||||
if typeMap[k] == "string" {
|
|
||||||
v = fmt.Sprintf("'%s'", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
keys = append(keys, k)
|
|
||||||
values = append(values, v)
|
|
||||||
}
|
|
||||||
return strings.Join(keys, ", "), strings.Join(values, ", ")
|
|
||||||
}
|
|
||||||
|
@@ -52,7 +52,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "cross-env PORT=7001 craco start",
|
"start": "cross-env PORT=7001 craco start",
|
||||||
"build": "craco --max_old_space_size=4096 build",
|
"build": "craco build",
|
||||||
"test": "craco test",
|
"test": "craco test",
|
||||||
"eject": "craco eject",
|
"eject": "craco eject",
|
||||||
"crowdin:sync": "crowdin upload && crowdin download",
|
"crowdin:sync": "crowdin upload && crowdin download",
|
||||||
|
@@ -89,6 +89,7 @@ import ThemeSelect from "./common/select/ThemeSelect";
|
|||||||
import OrganizationSelect from "./common/select/OrganizationSelect";
|
import OrganizationSelect from "./common/select/OrganizationSelect";
|
||||||
import {clearWeb3AuthToken} from "./auth/Web3Auth";
|
import {clearWeb3AuthToken} from "./auth/Web3Auth";
|
||||||
import AccountAvatar from "./account/AccountAvatar";
|
import AccountAvatar from "./account/AccountAvatar";
|
||||||
|
import OpenTour from "./common/OpenTour";
|
||||||
|
|
||||||
const {Header, Footer, Content} = Layout;
|
const {Header, Footer, Content} = Layout;
|
||||||
|
|
||||||
@@ -379,6 +380,7 @@ class App extends Component {
|
|||||||
});
|
});
|
||||||
}} />
|
}} />
|
||||||
<LanguageSelect languages={this.state.account.organization.languages} />
|
<LanguageSelect languages={this.state.account.organization.languages} />
|
||||||
|
<OpenTour />
|
||||||
{Setting.isAdminUser(this.state.account) && !Setting.isMobile() &&
|
{Setting.isAdminUser(this.state.account) && !Setting.isMobile() &&
|
||||||
<OrganizationSelect
|
<OrganizationSelect
|
||||||
initValue={Setting.getOrganization()}
|
initValue={Setting.getOrganization()}
|
||||||
@@ -448,7 +450,7 @@ class App extends Component {
|
|||||||
|
|
||||||
res.push(Setting.getItem(<Link style={{color: "black"}} to="/sessions">{i18next.t("general:Logging & Auditing")}</Link>, "/logs", <WalletTwoTone />, [
|
res.push(Setting.getItem(<Link style={{color: "black"}} to="/sessions">{i18next.t("general:Logging & Auditing")}</Link>, "/logs", <WalletTwoTone />, [
|
||||||
Setting.getItem(<Link to="/sessions">{i18next.t("general:Sessions")}</Link>, "/sessions"),
|
Setting.getItem(<Link to="/sessions">{i18next.t("general:Sessions")}</Link>, "/sessions"),
|
||||||
Setting.getItem(<a target="_blank" rel="noreferrer" href={"http://localhost:18001/records"}>{i18next.t("general:Records")}</a>, "/records"),
|
Setting.getItem(<a target="_blank" rel="noreferrer" href={Conf.CasvisorUrl}>{i18next.t("general:Records")}</a>, "/records"),
|
||||||
Setting.getItem(<Link to="/tokens">{i18next.t("general:Tokens")}</Link>, "/tokens"),
|
Setting.getItem(<Link to="/tokens">{i18next.t("general:Tokens")}</Link>, "/tokens"),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
@@ -460,11 +462,17 @@ class App extends Component {
|
|||||||
Setting.getItem(<Link to="/subscriptions">{i18next.t("general:Subscriptions")}</Link>, "/subscriptions"),
|
Setting.getItem(<Link to="/subscriptions">{i18next.t("general:Subscriptions")}</Link>, "/subscriptions"),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
if (Setting.isAdminUser(this.state.account)) {
|
||||||
res.push(Setting.getItem(<Link style={{color: "black"}} to="/sysinfo">{i18next.t("general:Admin")}</Link>, "/admin", <SettingTwoTone />, [
|
res.push(Setting.getItem(<Link style={{color: "black"}} to="/sysinfo">{i18next.t("general:Admin")}</Link>, "/admin", <SettingTwoTone />, [
|
||||||
Setting.getItem(<Link to="/sysinfo">{i18next.t("general:System Info")}</Link>, "/sysinfo"),
|
Setting.getItem(<Link to="/sysinfo">{i18next.t("general:System Info")}</Link>, "/sysinfo"),
|
||||||
Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"),
|
Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"),
|
||||||
Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>, "/webhooks"),
|
Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>, "/webhooks"),
|
||||||
Setting.getItem(<a target="_blank" rel="noreferrer" href={Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger"}>{i18next.t("general:Swagger")}</a>, "/swagger")]));
|
Setting.getItem(<a target="_blank" rel="noreferrer" href={Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger"}>{i18next.t("general:Swagger")}</a>, "/swagger")]));
|
||||||
|
} else {
|
||||||
|
res.push(Setting.getItem(<Link style={{color: "black"}} to="/syncers">{i18next.t("general:Admin")}</Link>, "/admin", <SettingTwoTone />, [
|
||||||
|
Setting.getItem(<Link to="/syncers">{i18next.t("general:Syncers")}</Link>, "/syncers"),
|
||||||
|
Setting.getItem(<Link to="/webhooks">{i18next.t("general:Webhooks")}</Link>, "/webhooks")]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
@@ -563,9 +571,7 @@ class App extends Component {
|
|||||||
|
|
||||||
renderContent() {
|
renderContent() {
|
||||||
const onClick = ({key}) => {
|
const onClick = ({key}) => {
|
||||||
if (key === "/swagger") {
|
if (key !== "/swagger" && key !== "/records") {
|
||||||
window.open(Setting.isLocalhost() ? `${Setting.ServerUrl}/swagger` : "/swagger", "_blank");
|
|
||||||
} else {
|
|
||||||
if (this.state.requiredEnableMfa) {
|
if (this.state.requiredEnableMfa) {
|
||||||
Setting.showMessage("info", "Please enable MFA first!");
|
Setting.showMessage("info", "Please enable MFA first!");
|
||||||
} else {
|
} else {
|
||||||
|
@@ -13,11 +13,12 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Button, Input, Result, Space} from "antd";
|
import {Button, Input, Result, Space, Tour} from "antd";
|
||||||
import {SearchOutlined} from "@ant-design/icons";
|
import {SearchOutlined} from "@ant-design/icons";
|
||||||
import Highlighter from "react-highlight-words";
|
import Highlighter from "react-highlight-words";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import * as Setting from "./Setting";
|
import * as Setting from "./Setting";
|
||||||
|
import * as TourConfig from "./TourConfig";
|
||||||
|
|
||||||
class BaseListPage extends React.Component {
|
class BaseListPage extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -33,6 +34,7 @@ class BaseListPage extends React.Component {
|
|||||||
searchText: "",
|
searchText: "",
|
||||||
searchedColumn: "",
|
searchedColumn: "",
|
||||||
isAuthorized: true,
|
isAuthorized: true,
|
||||||
|
isTourVisible: TourConfig.getTourVisible(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,14 +43,23 @@ class BaseListPage extends React.Component {
|
|||||||
this.fetch({pagination});
|
this.fetch({pagination});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleTourChange = () => {
|
||||||
|
this.setState({isTourVisible: TourConfig.getTourVisible()});
|
||||||
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
window.addEventListener("storageOrganizationChanged", this.handleOrganizationChange);
|
window.addEventListener("storageOrganizationChanged", this.handleOrganizationChange);
|
||||||
|
window.addEventListener("storageTourChanged", this.handleTourChange);
|
||||||
if (!Setting.isAdminUser(this.props.account)) {
|
if (!Setting.isAdminUser(this.props.account)) {
|
||||||
Setting.setOrganization("All");
|
Setting.setOrganization("All");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
if (this.state.intervalId !== null) {
|
||||||
|
clearInterval(this.state.intervalId);
|
||||||
|
}
|
||||||
|
window.removeEventListener("storageTourChanged", this.handleTourChange);
|
||||||
window.removeEventListener("storageOrganizationChanged", this.handleOrganizationChange);
|
window.removeEventListener("storageOrganizationChanged", this.handleOrganizationChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +155,37 @@ class BaseListPage extends React.Component {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setIsTourVisible = () => {
|
||||||
|
TourConfig.setIsTourVisible(false);
|
||||||
|
this.setState({isTourVisible: false});
|
||||||
|
};
|
||||||
|
|
||||||
|
getSteps = () => {
|
||||||
|
const nextPathName = TourConfig.getNextUrl();
|
||||||
|
const steps = TourConfig.getSteps();
|
||||||
|
steps.map((item, index) => {
|
||||||
|
if (!index) {
|
||||||
|
item.target = () => document.querySelector("table");
|
||||||
|
} else {
|
||||||
|
item.target = () => document.getElementById(item.id) || null;
|
||||||
|
}
|
||||||
|
if (index === steps.length - 1) {
|
||||||
|
item.nextButtonProps = {
|
||||||
|
children: TourConfig.getNextButtonChild(nextPathName),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return steps;
|
||||||
|
};
|
||||||
|
|
||||||
|
handleTourComplete = () => {
|
||||||
|
const nextPathName = TourConfig.getNextUrl();
|
||||||
|
if (nextPathName !== "") {
|
||||||
|
this.props.history.push("/" + nextPathName);
|
||||||
|
TourConfig.setIsTourVisible(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.state.isAuthorized) {
|
if (!this.state.isAuthorized) {
|
||||||
return (
|
return (
|
||||||
@@ -161,6 +203,17 @@ class BaseListPage extends React.Component {
|
|||||||
{
|
{
|
||||||
this.renderTable(this.state.data)
|
this.renderTable(this.state.data)
|
||||||
}
|
}
|
||||||
|
<Tour
|
||||||
|
open={this.state.isTourVisible}
|
||||||
|
onClose={this.setIsTourVisible}
|
||||||
|
steps={this.getSteps()}
|
||||||
|
indicatorsRender={(current, total) => (
|
||||||
|
<span>
|
||||||
|
{current + 1} / {total}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
onFinish={this.handleTourComplete}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,8 @@
|
|||||||
|
|
||||||
export const DefaultApplication = "app-built-in";
|
export const DefaultApplication = "app-built-in";
|
||||||
|
|
||||||
|
export const CasvisorUrl = "https://github.com/casbin/casvisor";
|
||||||
|
|
||||||
export const ShowGithubCorner = false;
|
export const ShowGithubCorner = false;
|
||||||
export const IsDemoMode = false;
|
export const IsDemoMode = false;
|
||||||
|
|
||||||
|
@@ -101,6 +101,21 @@ class PaymentListPage extends BaseListPage {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: i18next.t("general:Organization"),
|
||||||
|
dataIndex: "owner",
|
||||||
|
key: "owner",
|
||||||
|
width: "120px",
|
||||||
|
sorter: true,
|
||||||
|
...this.getColumnSearchProps("owner"),
|
||||||
|
render: (text, record, index) => {
|
||||||
|
return (
|
||||||
|
<Link to={`/organizations/${text}`}>
|
||||||
|
{text}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("general:Provider"),
|
title: i18next.t("general:Provider"),
|
||||||
dataIndex: "provider",
|
dataIndex: "provider",
|
||||||
@@ -117,21 +132,6 @@ class PaymentListPage extends BaseListPage {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: i18next.t("general:Organization"),
|
|
||||||
dataIndex: "owner",
|
|
||||||
key: "owner",
|
|
||||||
width: "120px",
|
|
||||||
sorter: true,
|
|
||||||
...this.getColumnSearchProps("owner"),
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return (
|
|
||||||
<Link to={`/organizations/${text}`}>
|
|
||||||
{text}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: i18next.t("general:User"),
|
title: i18next.t("general:User"),
|
||||||
dataIndex: "user",
|
dataIndex: "user",
|
||||||
@@ -264,7 +264,7 @@ class PaymentListPage extends BaseListPage {
|
|||||||
value = params.type;
|
value = params.type;
|
||||||
}
|
}
|
||||||
this.setState({loading: true});
|
this.setState({loading: true});
|
||||||
PaymentBackend.getPayments(Setting.getRequestOrganization(this.props.account), Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
PaymentBackend.getPayments(Setting.isDefaultOrganizationSelected(this.props.account) ? "" : Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@@ -110,11 +110,12 @@ class PermissionListPage extends BaseListPage {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Upload {...props}>
|
<Upload {...props}>
|
||||||
<Button type="primary" size="small">
|
<Button id="upload-button" type="primary" size="small">
|
||||||
<UploadOutlined /> {i18next.t("user:Upload (.xlsx)")}
|
<UploadOutlined /> {i18next.t("user:Upload (.xlsx)")}
|
||||||
</Button></Upload>
|
</Button></Upload>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderTable(permissions) {
|
renderTable(permissions) {
|
||||||
const columns = [
|
const columns = [
|
||||||
// https://github.com/ant-design/ant-design/issues/22184
|
// https://github.com/ant-design/ant-design/issues/22184
|
||||||
@@ -361,7 +362,7 @@ class PermissionListPage extends BaseListPage {
|
|||||||
title={() => (
|
title={() => (
|
||||||
<div>
|
<div>
|
||||||
{i18next.t("general:Permissions")}
|
{i18next.t("general:Permissions")}
|
||||||
<Button style={{marginRight: "5px"}} type="primary" size="small" onClick={this.addPermission.bind(this)}>{i18next.t("general:Add")}</Button>
|
<Button id="add-button" style={{marginRight: "5px"}} type="primary" size="small" onClick={this.addPermission.bind(this)}>{i18next.t("general:Add")}</Button>
|
||||||
{
|
{
|
||||||
this.renderPermissionUpload()
|
this.renderPermissionUpload()
|
||||||
}
|
}
|
||||||
|
@@ -197,22 +197,29 @@ class PlanEditPage extends React.Component {
|
|||||||
</Row>
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("plan:Price per month"), i18next.t("plan:Price per month - Tooltip"))} :
|
{Setting.getLabel(i18next.t("plan:Price"), i18next.t("plan:Price - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<InputNumber value={this.state.plan.pricePerMonth} onChange={value => {
|
<InputNumber value={this.state.plan.price} onChange={value => {
|
||||||
this.updatePlanField("pricePerMonth", value);
|
this.updatePlanField("price", value);
|
||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("plan:Price per year"), i18next.t("plan:Price per year - Tooltip"))} :
|
{Setting.getLabel(i18next.t("plan:Period"), i18next.t("plan:Period - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<InputNumber value={this.state.plan.pricePerYear} onChange={value => {
|
<Select
|
||||||
this.updatePlanField("pricePerYear", value);
|
defaultValue={this.state.plan.period === "" ? "Monthly" : this.state.plan.period}
|
||||||
}} />
|
onChange={value => {
|
||||||
|
this.updatePlanField("period", value);
|
||||||
|
}}
|
||||||
|
options={[
|
||||||
|
{value: "Monthly", label: "Monthly"},
|
||||||
|
{value: "Yearly", label: "Yearly"},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
@@ -32,10 +32,11 @@ class PlanListPage extends BaseListPage {
|
|||||||
createdTime: moment().format(),
|
createdTime: moment().format(),
|
||||||
displayName: `New Plan - ${randomName}`,
|
displayName: `New Plan - ${randomName}`,
|
||||||
description: "",
|
description: "",
|
||||||
pricePerMonth: 10,
|
price: 10,
|
||||||
pricePerYear: 100,
|
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
|
period: "Monthly",
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
|
paymentProviders: [],
|
||||||
role: "",
|
role: "",
|
||||||
options: [],
|
options: [],
|
||||||
};
|
};
|
||||||
@@ -135,18 +136,18 @@ class PlanListPage extends BaseListPage {
|
|||||||
...this.getColumnSearchProps("currency"),
|
...this.getColumnSearchProps("currency"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("plan:Price per month"),
|
title: i18next.t("plan:Price"),
|
||||||
dataIndex: "pricePerMonth",
|
dataIndex: "price",
|
||||||
key: "pricePerMonth",
|
key: "price",
|
||||||
width: "130px",
|
width: "130px",
|
||||||
...this.getColumnSearchProps("pricePerMonth"),
|
...this.getColumnSearchProps("price"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("plan:Price per year"),
|
title: i18next.t("plan:Period"),
|
||||||
dataIndex: "pricePerYear",
|
dataIndex: "period",
|
||||||
key: "pricePerYear",
|
key: "period",
|
||||||
width: "130px",
|
width: "130px",
|
||||||
...this.getColumnSearchProps("pricePerYear"),
|
...this.getColumnSearchProps("period"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("general:Role"),
|
title: i18next.t("general:Role"),
|
||||||
|
@@ -76,11 +76,10 @@ class ProductBuyPage extends React.Component {
|
|||||||
throw new Error(res.msg);
|
throw new Error(res.msg);
|
||||||
}
|
}
|
||||||
const plan = res.data;
|
const plan = res.data;
|
||||||
const productName = plan.product;
|
|
||||||
await this.setStateAsync({
|
await this.setStateAsync({
|
||||||
pricing: pricing,
|
pricing: pricing,
|
||||||
plan: plan,
|
plan: plan,
|
||||||
productName: productName,
|
productName: plan.product,
|
||||||
});
|
});
|
||||||
this.onUpdatePricing(pricing);
|
this.onUpdatePricing(pricing);
|
||||||
}
|
}
|
||||||
|
@@ -98,6 +98,7 @@ class ProductEditPage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderProduct() {
|
renderProduct() {
|
||||||
|
const isCreatedByPlan = this.state.product.tag === "auto_created_product_for_plan";
|
||||||
return (
|
return (
|
||||||
<Card size="small" title={
|
<Card size="small" title={
|
||||||
<div>
|
<div>
|
||||||
@@ -107,12 +108,24 @@ class ProductEditPage extends React.Component {
|
|||||||
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteProduct()}>{i18next.t("general:Cancel")}</Button> : null}
|
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteProduct()}>{i18next.t("general:Cancel")}</Button> : null}
|
||||||
</div>
|
</div>
|
||||||
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
|
} style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
|
||||||
|
<Row style={{marginTop: "10px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account) || isCreatedByPlan} value={this.state.product.owner} onChange={(value => {this.updateProductField("owner", value);})}>
|
||||||
|
{
|
||||||
|
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
|
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Input value={this.state.product.name} onChange={e => {
|
<Input value={this.state.product.name} disabled={isCreatedByPlan} onChange={e => {
|
||||||
this.updateProductField("name", e.target.value);
|
this.updateProductField("name", e.target.value);
|
||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
@@ -127,18 +140,6 @@ class ProductEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
|
||||||
{Setting.getLabel(i18next.t("general:Organization"), i18next.t("general:Organization - Tooltip"))} :
|
|
||||||
</Col>
|
|
||||||
<Col span={22} >
|
|
||||||
<Select virtual={false} style={{width: "100%"}} disabled={!Setting.isAdminUser(this.props.account)} value={this.state.product.owner} onChange={(value => {this.updateProductField("owner", value);})}>
|
|
||||||
{
|
|
||||||
this.state.organizations.map((organization, index) => <Option key={index} value={organization.name}>{organization.name}</Option>)
|
|
||||||
}
|
|
||||||
</Select>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("product:Image"), i18next.t("product:Image - Tooltip"))} :
|
{Setting.getLabel(i18next.t("product:Image"), i18next.t("product:Image - Tooltip"))} :
|
||||||
@@ -171,7 +172,7 @@ class ProductEditPage extends React.Component {
|
|||||||
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("product:Tag - Tooltip"))} :
|
{Setting.getLabel(i18next.t("user:Tag"), i18next.t("product:Tag - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Input value={this.state.product.tag} onChange={e => {
|
<Input value={this.state.product.tag} disabled={isCreatedByPlan} onChange={e => {
|
||||||
this.updateProductField("tag", e.target.value);
|
this.updateProductField("tag", e.target.value);
|
||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
@@ -201,7 +202,7 @@ class ProductEditPage extends React.Component {
|
|||||||
{Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} :
|
{Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Select virtual={false} style={{width: "100%"}} value={this.state.product.currency} onChange={(value => {
|
<Select virtual={false} style={{width: "100%"}} value={this.state.product.currency} disabled={isCreatedByPlan} onChange={(value => {
|
||||||
this.updateProductField("currency", value);
|
this.updateProductField("currency", value);
|
||||||
})}>
|
})}>
|
||||||
{
|
{
|
||||||
@@ -218,7 +219,7 @@ class ProductEditPage extends React.Component {
|
|||||||
{Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} :
|
{Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<InputNumber value={this.state.product.price} onChange={value => {
|
<InputNumber value={this.state.product.price} disabled={isCreatedByPlan} onChange={value => {
|
||||||
this.updateProductField("price", value);
|
this.updateProductField("price", value);
|
||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
@@ -228,7 +229,7 @@ class ProductEditPage extends React.Component {
|
|||||||
{Setting.getLabel(i18next.t("product:Quantity"), i18next.t("product:Quantity - Tooltip"))} :
|
{Setting.getLabel(i18next.t("product:Quantity"), i18next.t("product:Quantity - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<InputNumber value={this.state.product.quantity} onChange={value => {
|
<InputNumber value={this.state.product.quantity} disabled={isCreatedByPlan} onChange={value => {
|
||||||
this.updateProductField("quantity", value);
|
this.updateProductField("quantity", value);
|
||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
@@ -238,7 +239,7 @@ class ProductEditPage extends React.Component {
|
|||||||
{Setting.getLabel(i18next.t("product:Sold"), i18next.t("product:Sold - Tooltip"))} :
|
{Setting.getLabel(i18next.t("product:Sold"), i18next.t("product:Sold - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<InputNumber value={this.state.product.sold} onChange={value => {
|
<InputNumber value={this.state.product.sold} disabled={isCreatedByPlan} onChange={value => {
|
||||||
this.updateProductField("sold", value);
|
this.updateProductField("sold", value);
|
||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
@@ -248,7 +249,7 @@ class ProductEditPage extends React.Component {
|
|||||||
{Setting.getLabel(i18next.t("product:Payment providers"), i18next.t("product:Payment providers - Tooltip"))} :
|
{Setting.getLabel(i18next.t("product:Payment providers"), i18next.t("product:Payment providers - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Select virtual={false} mode="multiple" style={{width: "100%"}} value={this.state.product.providers} onChange={(value => {this.updateProductField("providers", value);})}>
|
<Select virtual={false} mode="multiple" style={{width: "100%"}} disabled={isCreatedByPlan} value={this.state.product.providers} onChange={(value => {this.updateProductField("providers", value);})}>
|
||||||
{
|
{
|
||||||
this.state.providers.map((provider, index) => <Option key={index} value={provider.name}>{provider.name}</Option>)
|
this.state.providers.map((provider, index) => <Option key={index} value={provider.name}>{provider.name}</Option>)
|
||||||
}
|
}
|
||||||
@@ -312,7 +313,7 @@ class ProductEditPage extends React.Component {
|
|||||||
|
|
||||||
submitProductEdit(willExist) {
|
submitProductEdit(willExist) {
|
||||||
const product = Setting.deepCopy(this.state.product);
|
const product = Setting.deepCopy(this.state.product);
|
||||||
ProductBackend.updateProduct(this.state.product.owner, this.state.productName, product)
|
ProductBackend.updateProduct(this.state.organizationName, this.state.productName, product)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
Setting.showMessage("success", i18next.t("general:Successfully saved"));
|
Setting.showMessage("success", i18next.t("general:Successfully saved"));
|
||||||
|
@@ -253,11 +253,13 @@ class ProductListPage extends BaseListPage {
|
|||||||
width: "230px",
|
width: "230px",
|
||||||
fixed: (Setting.isMobile()) ? "false" : "right",
|
fixed: (Setting.isMobile()) ? "false" : "right",
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
|
const isCreatedByPlan = record.tag === "auto_created_product_for_plan";
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => this.props.history.push(`/products/${record.owner}/${record.name}/buy`)}>{i18next.t("product:Buy")}</Button>
|
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} onClick={() => this.props.history.push(`/products/${record.owner}/${record.name}/buy`)}>{i18next.t("product:Buy")}</Button>
|
||||||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/products/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/products/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
|
||||||
<PopconfirmModal
|
<PopconfirmModal
|
||||||
|
disabled={isCreatedByPlan}
|
||||||
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
|
||||||
onConfirm={() => this.deleteProduct(index)}
|
onConfirm={() => this.deleteProduct(index)}
|
||||||
>
|
>
|
||||||
|
@@ -423,13 +423,13 @@ class ProviderEditPage extends React.Component {
|
|||||||
[
|
[
|
||||||
{id: "Captcha", name: "Captcha"},
|
{id: "Captcha", name: "Captcha"},
|
||||||
{id: "Email", name: "Email"},
|
{id: "Email", name: "Email"},
|
||||||
|
{id: "Notification", name: "Notification"},
|
||||||
{id: "OAuth", name: "OAuth"},
|
{id: "OAuth", name: "OAuth"},
|
||||||
{id: "Payment", name: "Payment"},
|
{id: "Payment", name: "Payment"},
|
||||||
{id: "SAML", name: "SAML"},
|
{id: "SAML", name: "SAML"},
|
||||||
{id: "SMS", name: "SMS"},
|
{id: "SMS", name: "SMS"},
|
||||||
{id: "Storage", name: "Storage"},
|
{id: "Storage", name: "Storage"},
|
||||||
{id: "Web3", name: "Web3"},
|
{id: "Web3", name: "Web3"},
|
||||||
{id: "Notification", name: "Notification"},
|
|
||||||
]
|
]
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))
|
.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
.map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)
|
.map((providerCategory, index) => <Option key={index} value={providerCategory.id}>{providerCategory.name}</Option>)
|
||||||
|
@@ -142,13 +142,15 @@ class ProviderListPage extends BaseListPage {
|
|||||||
key: "category",
|
key: "category",
|
||||||
filterMultiple: false,
|
filterMultiple: false,
|
||||||
filters: [
|
filters: [
|
||||||
{text: "OAuth", value: "OAuth"},
|
{text: "Captcha", value: "Captcha"},
|
||||||
{text: "Email", value: "Email"},
|
{text: "Email", value: "Email"},
|
||||||
|
{text: "Notification", value: "Notification"},
|
||||||
|
{text: "OAuth", value: "OAuth"},
|
||||||
|
{text: "Payment", value: "Payment"},
|
||||||
|
{text: "SAML", value: "SAML"},
|
||||||
{text: "SMS", value: "SMS"},
|
{text: "SMS", value: "SMS"},
|
||||||
{text: "Storage", value: "Storage"},
|
{text: "Storage", value: "Storage"},
|
||||||
{text: "SAML", value: "SAML"},
|
{text: "Web3", value: "Web3"},
|
||||||
{text: "Captcha", value: "Captcha"},
|
|
||||||
{text: "Payment", value: "Payment"},
|
|
||||||
],
|
],
|
||||||
width: "110px",
|
width: "110px",
|
||||||
sorter: true,
|
sorter: true,
|
||||||
@@ -161,13 +163,15 @@ class ProviderListPage extends BaseListPage {
|
|||||||
align: "center",
|
align: "center",
|
||||||
filterMultiple: false,
|
filterMultiple: false,
|
||||||
filters: [
|
filters: [
|
||||||
{text: "OAuth", value: "OAuth", children: Setting.getProviderTypeOptions("OAuth").map((o) => {return {text: o.id, value: o.name};})},
|
{text: "Captcha", value: "Captcha", children: Setting.getProviderTypeOptions("Captcha").map((o) => {return {text: o.id, value: o.name};})},
|
||||||
{text: "Email", value: "Email", children: Setting.getProviderTypeOptions("Email").map((o) => {return {text: o.id, value: o.name};})},
|
{text: "Email", value: "Email", children: Setting.getProviderTypeOptions("Email").map((o) => {return {text: o.id, value: o.name};})},
|
||||||
|
{text: "Notification", value: "Notification", children: Setting.getProviderTypeOptions("Notification").map((o) => {return {text: o.id, value: o.name};})},
|
||||||
|
{text: "OAuth", value: "OAuth", children: Setting.getProviderTypeOptions("OAuth").map((o) => {return {text: o.id, value: o.name};})},
|
||||||
|
{text: "Payment", value: "Payment", children: Setting.getProviderTypeOptions("Payment").map((o) => {return {text: o.id, value: o.name};})},
|
||||||
|
{text: "SAML", value: "SAML", children: Setting.getProviderTypeOptions("SAML").map((o) => {return {text: o.id, value: o.name};})},
|
||||||
{text: "SMS", value: "SMS", children: Setting.getProviderTypeOptions("SMS").map((o) => {return {text: o.id, value: o.name};})},
|
{text: "SMS", value: "SMS", children: Setting.getProviderTypeOptions("SMS").map((o) => {return {text: o.id, value: o.name};})},
|
||||||
{text: "Storage", value: "Storage", children: Setting.getProviderTypeOptions("Storage").map((o) => {return {text: o.id, value: o.name};})},
|
{text: "Storage", value: "Storage", children: Setting.getProviderTypeOptions("Storage").map((o) => {return {text: o.id, value: o.name};})},
|
||||||
{text: "SAML", value: "SAML", children: Setting.getProviderTypeOptions("SAML").map((o) => {return {text: o.id, value: o.name};})},
|
{text: "Web3", value: "Web3", children: Setting.getProviderTypeOptions("Web3").map((o) => {return {text: o.id, value: o.name};})},
|
||||||
{text: "Captcha", value: "Captcha", children: Setting.getProviderTypeOptions("Captcha").map((o) => {return {text: o.id, value: o.name};})},
|
|
||||||
{text: "Payment", value: "Payment", children: Setting.getProviderTypeOptions("Payment").map((o) => {return {text: o.id, value: o.name};})},
|
|
||||||
],
|
],
|
||||||
sorter: true,
|
sorter: true,
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
@@ -237,7 +241,7 @@ class ProviderListPage extends BaseListPage {
|
|||||||
title={() => (
|
title={() => (
|
||||||
<div>
|
<div>
|
||||||
{i18next.t("general:Providers")}
|
{i18next.t("general:Providers")}
|
||||||
<Button type="primary" size="small" onClick={this.addProvider.bind(this)}>{i18next.t("general:Add")}</Button>
|
<Button id="add-button" type="primary" size="small" onClick={this.addProvider.bind(this)}>{i18next.t("general:Add")}</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
loading={this.state.loading}
|
loading={this.state.loading}
|
||||||
|
@@ -76,7 +76,7 @@ class ResourceListPage extends BaseListPage {
|
|||||||
return (
|
return (
|
||||||
<Upload maxCount={1} accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.csv,.xls,.xlsx" showUploadList={false}
|
<Upload maxCount={1} accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.csv,.xls,.xlsx" showUploadList={false}
|
||||||
beforeUpload={file => {return false;}} onChange={info => {this.handleUpload(info);}}>
|
beforeUpload={file => {return false;}} onChange={info => {this.handleUpload(info);}}>
|
||||||
<Button icon={<UploadOutlined />} loading={this.state.uploading} type="primary" size="small">
|
<Button id="upload-button" icon={<UploadOutlined />} loading={this.state.uploading} type="primary" size="small">
|
||||||
{i18next.t("resource:Upload a file...")}
|
{i18next.t("resource:Upload a file...")}
|
||||||
</Button>
|
</Button>
|
||||||
</Upload>
|
</Upload>
|
||||||
|
@@ -274,7 +274,7 @@ export const OtherProviderInfo = {
|
|||||||
},
|
},
|
||||||
"Custom HTTP": {
|
"Custom HTTP": {
|
||||||
logo: `${StaticBaseUrl}/img/email_default.png`,
|
logo: `${StaticBaseUrl}/img/email_default.png`,
|
||||||
url: "https://casdoor.org/docs/provider/sms/overview",
|
url: "https://casdoor.org/docs/provider/notification/overview",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -927,7 +927,7 @@ export function getProviderTypeOptions(category) {
|
|||||||
{id: "Local File System", name: "Local File System"},
|
{id: "Local File System", name: "Local File System"},
|
||||||
{id: "AWS S3", name: "AWS S3"},
|
{id: "AWS S3", name: "AWS S3"},
|
||||||
{id: "MinIO", name: "MinIO"},
|
{id: "MinIO", name: "MinIO"},
|
||||||
{id: "Aliyun OSS", name: "Aliyun OSS"},
|
{id: "Aliyun OSS", name: "Alibaba Cloud OSS"},
|
||||||
{id: "Tencent Cloud COS", name: "Tencent Cloud COS"},
|
{id: "Tencent Cloud COS", name: "Tencent Cloud COS"},
|
||||||
{id: "Azure Blob", name: "Azure Blob"},
|
{id: "Azure Blob", name: "Azure Blob"},
|
||||||
{id: "Qiniu Cloud Kodo", name: "Qiniu Cloud Kodo"},
|
{id: "Qiniu Cloud Kodo", name: "Qiniu Cloud Kodo"},
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Button, Card, Col, DatePicker, Input, InputNumber, Row, Select} from "antd";
|
import {Button, Card, Col, DatePicker, Input, Row, Select} from "antd";
|
||||||
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
import * as OrganizationBackend from "./backend/OrganizationBackend";
|
||||||
import * as PricingBackend from "./backend/PricingBackend";
|
import * as PricingBackend from "./backend/PricingBackend";
|
||||||
import * as PlanBackend from "./backend/PlanBackend";
|
import * as PlanBackend from "./backend/PlanBackend";
|
||||||
@@ -171,16 +171,6 @@ class SubscriptionEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
|
||||||
{Setting.getLabel(i18next.t("subscription:Duration"), i18next.t("subscription:Duration - Tooltip"))}
|
|
||||||
</Col>
|
|
||||||
<Col span={22} >
|
|
||||||
<InputNumber value={this.state.subscription.duration} onChange={value => {
|
|
||||||
this.updateSubscriptionField("duration", value);
|
|
||||||
}} />
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("subscription:Start time"), i18next.t("subscription:Start time - Tooltip"))}
|
{Setting.getLabel(i18next.t("subscription:Start time"), i18next.t("subscription:Start time - Tooltip"))}
|
||||||
@@ -201,6 +191,23 @@ class SubscriptionEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("plan:Period"), i18next.t("plan:Period - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<Select
|
||||||
|
defaultValue={this.state.subscription.period === "" ? "Monthly" : this.state.subscription.period}
|
||||||
|
onChange={value => {
|
||||||
|
this.updateSubscriptionField("period", value);
|
||||||
|
}}
|
||||||
|
options={[
|
||||||
|
{value: "Monthly", label: "Monthly"},
|
||||||
|
{value: "Yearly", label: "Yearly"},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("general:User"), i18next.t("general:User - Tooltip"))} :
|
{Setting.getLabel(i18next.t("general:User"), i18next.t("general:User - Tooltip"))} :
|
||||||
|
@@ -27,7 +27,6 @@ class SubscriptionListPage extends BaseListPage {
|
|||||||
newSubscription() {
|
newSubscription() {
|
||||||
const randomName = Setting.getRandomName();
|
const randomName = Setting.getRandomName();
|
||||||
const owner = Setting.getRequestOrganization(this.props.account);
|
const owner = Setting.getRequestOrganization(this.props.account);
|
||||||
const defaultDuration = 30;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
owner: owner,
|
owner: owner,
|
||||||
@@ -35,8 +34,8 @@ class SubscriptionListPage extends BaseListPage {
|
|||||||
createdTime: moment().format(),
|
createdTime: moment().format(),
|
||||||
displayName: `New Subscription - ${randomName}`,
|
displayName: `New Subscription - ${randomName}`,
|
||||||
startTime: moment().format(),
|
startTime: moment().format(),
|
||||||
endTime: moment().add(defaultDuration, "d").format(),
|
endTime: moment().add(30, "d").format(),
|
||||||
duration: defaultDuration,
|
period: "Monthly",
|
||||||
description: "",
|
description: "",
|
||||||
user: "",
|
user: "",
|
||||||
plan: "",
|
plan: "",
|
||||||
@@ -130,11 +129,11 @@ class SubscriptionListPage extends BaseListPage {
|
|||||||
...this.getColumnSearchProps("displayName"),
|
...this.getColumnSearchProps("displayName"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("subscription:Duration"),
|
title: i18next.t("subscription:Period"),
|
||||||
dataIndex: "duration",
|
dataIndex: "period",
|
||||||
key: "duration",
|
key: "period",
|
||||||
width: "140px",
|
width: "140px",
|
||||||
...this.getColumnSearchProps("duration"),
|
...this.getColumnSearchProps("period"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("subscription:Start time"),
|
title: i18next.t("subscription:Start time"),
|
||||||
@@ -150,20 +149,6 @@ class SubscriptionListPage extends BaseListPage {
|
|||||||
width: "140px",
|
width: "140px",
|
||||||
...this.getColumnSearchProps("endTime"),
|
...this.getColumnSearchProps("endTime"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: i18next.t("general:Pricing"),
|
|
||||||
dataIndex: "pricing",
|
|
||||||
key: "pricing",
|
|
||||||
width: "140px",
|
|
||||||
...this.getColumnSearchProps("pricing"),
|
|
||||||
render: (text, record, index) => {
|
|
||||||
return (
|
|
||||||
<Link to={`/pricings/${record.owner}/${text}`}>
|
|
||||||
{text}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: i18next.t("general:Plan"),
|
title: i18next.t("general:Plan"),
|
||||||
dataIndex: "plan",
|
dataIndex: "plan",
|
||||||
|
@@ -234,12 +234,60 @@ class SyncerEditPage extends React.Component {
|
|||||||
</Select>
|
</Select>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("syncer:Database type"), i18next.t("syncer:Database type - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<Select virtual={false} style={{width: "100%"}} value={this.state.syncer.databaseType} onChange={(value => {
|
||||||
|
this.updateSyncerField("databaseType", value);
|
||||||
|
if (value === "postgres") {
|
||||||
|
this.updateSyncerField("sslMode", "disable");
|
||||||
|
} else {
|
||||||
|
this.updateSyncerField("sslMode", "");
|
||||||
|
}
|
||||||
|
})}>
|
||||||
|
{
|
||||||
|
[
|
||||||
|
{id: "mysql", name: "MySQL"},
|
||||||
|
{id: "postgres", name: "PostgreSQL"},
|
||||||
|
{id: "mssql", name: "SQL Server"},
|
||||||
|
{id: "oracle", name: "Oracle"},
|
||||||
|
{id: "sqlite3", name: "Sqlite 3"},
|
||||||
|
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{
|
||||||
|
this.state.syncer.databaseType !== "postgres" ? null : (
|
||||||
|
<Row style={{marginTop: "20px"}} >
|
||||||
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
|
{Setting.getLabel(i18next.t("syncer:SSL mode"), i18next.t("syncer:SSL mode - Tooltip"))} :
|
||||||
|
</Col>
|
||||||
|
<Col span={22} >
|
||||||
|
<Select virtual={false} style={{width: "100%"}} value={this.state.syncer.sslMode} onChange={(value => {this.updateSyncerField("sslMode", value);})}>
|
||||||
|
{
|
||||||
|
[
|
||||||
|
{id: "disable", name: "disable"},
|
||||||
|
// {id: "allow", name: "allow"},
|
||||||
|
// {id: "prefer", name: "prefer"},
|
||||||
|
{id: "require", name: "require"},
|
||||||
|
{id: "verify-ca", name: "verify-ca"},
|
||||||
|
{id: "verify-full", name: "verify-full"},
|
||||||
|
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>)
|
||||||
|
}
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)
|
||||||
|
}
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
|
{Setting.getLabel(i18next.t("provider:Host"), i18next.t("provider:Host - Tooltip"))} :
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={22} >
|
<Col span={22} >
|
||||||
<Input value={this.state.syncer.host} onChange={e => {
|
<Input prefix={<LinkOutlined />} value={this.state.syncer.host} onChange={e => {
|
||||||
this.updateSyncerField("host", e.target.value);
|
this.updateSyncerField("host", e.target.value);
|
||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
@@ -274,24 +322,6 @@ class SyncerEditPage extends React.Component {
|
|||||||
}} />
|
}} />
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Row style={{marginTop: "20px"}} >
|
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
|
||||||
{Setting.getLabel(i18next.t("syncer:Database type"), i18next.t("syncer:Database type - Tooltip"))} :
|
|
||||||
</Col>
|
|
||||||
<Col span={22} >
|
|
||||||
<Select virtual={false} style={{width: "100%"}} value={this.state.syncer.databaseType} onChange={(value => {this.updateSyncerField("databaseType", value);})}>
|
|
||||||
{
|
|
||||||
[
|
|
||||||
{id: "mysql", name: "MySQL"},
|
|
||||||
{id: "postgres", name: "PostgreSQL"},
|
|
||||||
{id: "mssql", name: "SQL Server"},
|
|
||||||
{id: "oracle", name: "Oracle"},
|
|
||||||
{id: "sqlite3", name: "Sqlite 3"},
|
|
||||||
].map((databaseType, index) => <Option key={index} value={databaseType.id}>{databaseType.name}</Option>)
|
|
||||||
}
|
|
||||||
</Select>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row style={{marginTop: "20px"}} >
|
<Row style={{marginTop: "20px"}} >
|
||||||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
|
||||||
{Setting.getLabel(i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} :
|
{Setting.getLabel(i18next.t("syncer:Database"), i18next.t("syncer:Database - Tooltip"))} :
|
||||||
|
@@ -86,8 +86,13 @@ class SyncerListPage extends BaseListPage {
|
|||||||
this.setState({loading: true});
|
this.setState({loading: true});
|
||||||
SyncerBackend.runSyncer("admin", this.state.data[i].name)
|
SyncerBackend.runSyncer("admin", this.state.data[i].name)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
if (res.status === "ok") {
|
||||||
this.setState({loading: false});
|
this.setState({loading: false});
|
||||||
Setting.showMessage("success", "Syncer sync users successfully");
|
Setting.showMessage("success", i18next.t("general:Successfully synced"));
|
||||||
|
} else {
|
||||||
|
this.setState({loading: false});
|
||||||
|
Setting.showMessage("error", `${i18next.t("general:Failed to sync")}: ${res.msg}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
@@ -151,6 +156,13 @@ class SyncerListPage extends BaseListPage {
|
|||||||
{text: "LDAP", value: "LDAP"},
|
{text: "LDAP", value: "LDAP"},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: i18next.t("syncer:Database type"),
|
||||||
|
dataIndex: "databaseType",
|
||||||
|
key: "databaseType",
|
||||||
|
width: "130px",
|
||||||
|
sorter: (a, b) => a.databaseType.localeCompare(b.databaseType),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: i18next.t("provider:Host"),
|
title: i18next.t("provider:Host"),
|
||||||
dataIndex: "host",
|
dataIndex: "host",
|
||||||
@@ -183,13 +195,6 @@ class SyncerListPage extends BaseListPage {
|
|||||||
sorter: true,
|
sorter: true,
|
||||||
...this.getColumnSearchProps("password"),
|
...this.getColumnSearchProps("password"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: i18next.t("syncer:Database type"),
|
|
||||||
dataIndex: "databaseType",
|
|
||||||
key: "databaseType",
|
|
||||||
width: "120px",
|
|
||||||
sorter: (a, b) => a.databaseType.localeCompare(b.databaseType),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: i18next.t("syncer:Database"),
|
title: i18next.t("syncer:Database"),
|
||||||
dataIndex: "database",
|
dataIndex: "database",
|
||||||
@@ -208,7 +213,7 @@ class SyncerListPage extends BaseListPage {
|
|||||||
title: i18next.t("syncer:Sync interval"),
|
title: i18next.t("syncer:Sync interval"),
|
||||||
dataIndex: "syncInterval",
|
dataIndex: "syncInterval",
|
||||||
key: "syncInterval",
|
key: "syncInterval",
|
||||||
width: "130px",
|
width: "140px",
|
||||||
sorter: true,
|
sorter: true,
|
||||||
...this.getColumnSearchProps("syncInterval"),
|
...this.getColumnSearchProps("syncInterval"),
|
||||||
},
|
},
|
||||||
|
@@ -12,10 +12,11 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import {Card, Col, Divider, Progress, Row, Spin} from "antd";
|
import {Card, Col, Divider, Progress, Row, Spin, Tour} from "antd";
|
||||||
import * as SystemBackend from "./backend/SystemInfo";
|
import * as SystemBackend from "./backend/SystemInfo";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import * as Setting from "./Setting";
|
import * as Setting from "./Setting";
|
||||||
|
import * as TourConfig from "./TourConfig";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import PrometheusInfoTable from "./table/PrometheusInfoTable";
|
import PrometheusInfoTable from "./table/PrometheusInfoTable";
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ class SystemInfo extends React.Component {
|
|||||||
prometheusInfo: {apiThroughput: [], apiLatency: [], totalThroughput: 0},
|
prometheusInfo: {apiThroughput: [], apiLatency: [], totalThroughput: 0},
|
||||||
intervalId: null,
|
intervalId: null,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
isTourVisible: TourConfig.getTourVisible(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,12 +69,48 @@ class SystemInfo extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
window.addEventListener("storageTourChanged", this.handleTourChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTourChange = () => {
|
||||||
|
this.setState({isTourVisible: TourConfig.getTourVisible()});
|
||||||
|
};
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
if (this.state.intervalId !== null) {
|
if (this.state.intervalId !== null) {
|
||||||
clearInterval(this.state.intervalId);
|
clearInterval(this.state.intervalId);
|
||||||
}
|
}
|
||||||
|
window.removeEventListener("storageTourChanged", this.handleTourChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsTourVisible = () => {
|
||||||
|
TourConfig.setIsTourVisible(false);
|
||||||
|
this.setState({isTourVisible: false});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleTourComplete = () => {
|
||||||
|
const nextPathName = TourConfig.getNextUrl();
|
||||||
|
if (nextPathName !== "") {
|
||||||
|
this.props.history.push("/" + nextPathName);
|
||||||
|
TourConfig.setIsTourVisible(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getSteps = () => {
|
||||||
|
const nextPathName = TourConfig.getNextUrl();
|
||||||
|
const steps = TourConfig.getSteps();
|
||||||
|
steps.map((item, index) => {
|
||||||
|
item.target = () => document.getElementById(item.id) || null;
|
||||||
|
if (index === steps.length - 1) {
|
||||||
|
item.nextButtonProps = {
|
||||||
|
children: TourConfig.getNextButtonChild(nextPathName),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return steps;
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const cpuUi = this.state.systemInfo.cpuUsage?.length <= 0 ? i18next.t("system:Failed to get CPU usage") :
|
const cpuUi = this.state.systemInfo.cpuUsage?.length <= 0 ? i18next.t("system:Failed to get CPU usage") :
|
||||||
this.state.systemInfo.cpuUsage.map((usage, i) => {
|
this.state.systemInfo.cpuUsage.map((usage, i) => {
|
||||||
@@ -99,33 +137,34 @@ class SystemInfo extends React.Component {
|
|||||||
|
|
||||||
if (!Setting.isMobile()) {
|
if (!Setting.isMobile()) {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Row>
|
<Row>
|
||||||
<Col span={6}></Col>
|
<Col span={6}></Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Row gutter={[10, 10]}>
|
<Row gutter={[10, 10]}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Card title={i18next.t("system:CPU Usage")} bordered={true} style={{textAlign: "center", height: "100%"}}>
|
<Card id="cpu-card" title={i18next.t("system:CPU Usage")} bordered={true} style={{textAlign: "center", height: "100%"}}>
|
||||||
{this.state.loading ? <Spin size="large" /> : cpuUi}
|
{this.state.loading ? <Spin size="large" /> : cpuUi}
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Card title={i18next.t("system:Memory Usage")} bordered={true} style={{textAlign: "center", height: "100%"}}>
|
<Card id="memory-card" title={i18next.t("system:Memory Usage")} bordered={true} style={{textAlign: "center", height: "100%"}}>
|
||||||
{this.state.loading ? <Spin size="large" /> : memUi}
|
{this.state.loading ? <Spin size="large" /> : memUi}
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Card title={i18next.t("system:API Latency")} bordered={true} style={{textAlign: "center", height: "100%"}}>
|
<Card id="latency-card" title={i18next.t("system:API Latency")} bordered={true} style={{textAlign: "center", height: "100%"}}>
|
||||||
{this.state.loading ? <Spin size="large" /> : latencyUi}
|
{this.state.loading ? <Spin size="large" /> : latencyUi}
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Card title={i18next.t("system:API Throughput")} bordered={true} style={{textAlign: "center", height: "100%"}}>
|
<Card id="throughput-card" title={i18next.t("system:API Throughput")} bordered={true} style={{textAlign: "center", height: "100%"}}>
|
||||||
{this.state.loading ? <Spin size="large" /> : throughputUi}
|
{this.state.loading ? <Spin size="large" /> : throughputUi}
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Card title={i18next.t("system:About Casdoor")} bordered={true} style={{textAlign: "center"}}>
|
<Card id="about-card" title={i18next.t("system:About Casdoor")} bordered={true} style={{textAlign: "center"}}>
|
||||||
<div>{i18next.t("system:An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS")}</div>
|
<div>{i18next.t("system:An Identity and Access Management (IAM) / Single-Sign-On (SSO) platform with web UI supporting OAuth 2.0, OIDC, SAML and CAS")}</div>
|
||||||
GitHub: <a target="_blank" rel="noreferrer" href="https://github.com/casdoor/casdoor">Casdoor</a>
|
GitHub: <a target="_blank" rel="noreferrer" href="https://github.com/casdoor/casdoor">Casdoor</a>
|
||||||
<br />
|
<br />
|
||||||
@@ -138,6 +177,18 @@ class SystemInfo extends React.Component {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col span={6}></Col>
|
<Col span={6}></Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Tour
|
||||||
|
open={this.state.isTourVisible}
|
||||||
|
onClose={this.setIsTourVisible}
|
||||||
|
steps={this.getSteps()}
|
||||||
|
indicatorsRender={(current, total) => (
|
||||||
|
<span>
|
||||||
|
{current + 1} / {total}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
onFinish={this.handleTourComplete}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
|
229
web/src/TourConfig.js
Normal file
229
web/src/TourConfig.js
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const TourObj = {
|
||||||
|
home: [
|
||||||
|
{
|
||||||
|
title: "Welcome to casdoor",
|
||||||
|
description: "You can learn more about the use of CasDoor at https://casdoor.org/.",
|
||||||
|
cover: (
|
||||||
|
<img
|
||||||
|
alt="casdoor.png"
|
||||||
|
src="https://cdn.casbin.org/img/casdoor-logo_1185x256.png"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Statistic cards",
|
||||||
|
description: "Here are four statistic cards for user information.",
|
||||||
|
id: "statistic",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Import users",
|
||||||
|
description: "You can add new users or update existing Casdoor users by uploading a XLSX file of user information.",
|
||||||
|
id: "echarts-chart",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
webhooks: [
|
||||||
|
{
|
||||||
|
title: "Webhook List",
|
||||||
|
description: "Event systems allow you to build integrations, which subscribe to certain events on Casdoor. When one of those event is triggered, we'll send a POST json payload to the configured URL. The application parsed the json payload and carry out the hooked function. Events consist of signup, login, logout, update users, which are stored in the action field of the record. Event systems can be used to update an external issue from users.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
syncers: [
|
||||||
|
{
|
||||||
|
title: "Syncer List",
|
||||||
|
description: "Casdoor stores users in user table. Don't worry about migrating your application user data into Casdoor, when you plan to use Casdoor as an authentication platform. Casdoor provides syncer to quickly help you sync user data to Casdoor.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sysinfo: [
|
||||||
|
{
|
||||||
|
title: "CPU Usage",
|
||||||
|
description: "You can see the CPU usage in real time.",
|
||||||
|
id: "cpu-card",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Memory Usage",
|
||||||
|
description: "You can see the Memory usage in real time.",
|
||||||
|
id: "memory-card",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "API Latency",
|
||||||
|
description: "You can see the usage statistics of each API latency in real time.",
|
||||||
|
id: "latency-card",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "API Throughput",
|
||||||
|
description: "You can see the usage statistics of each API throughput in real time.",
|
||||||
|
id: "throughput-card",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "About Casdoor",
|
||||||
|
description: "You can get more Casdoor information in this card.",
|
||||||
|
id: "about-card",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
subscriptions: [
|
||||||
|
{
|
||||||
|
title: "Subscription List",
|
||||||
|
description: "Subscription helps to manage user's selected plan that make easy to control application's features access.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pricings: [
|
||||||
|
{
|
||||||
|
title: "Price List",
|
||||||
|
description: "Casdoor can be used as subscription management system via plan, pricing and subscription.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plans: [
|
||||||
|
{
|
||||||
|
title: "Plan List",
|
||||||
|
description: "Plan describe list of application's features with own name and price. Plan features depends on Casdoor role with set of permissions.That allow to describe plan's features independ on naming and price. For example: plan may has diffrent prices depends on county or date.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
payments: [
|
||||||
|
{
|
||||||
|
title: "Payment List",
|
||||||
|
description: "After the payment is successful, you can see the transaction information of the products in Payment, such as organization, user, purchase time, product name, etc.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
{
|
||||||
|
title: "Session List",
|
||||||
|
description: "You can add the product (or service) you want to sell. The following will tell you how to add a product.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
sessions: [
|
||||||
|
{
|
||||||
|
title: "Session List",
|
||||||
|
description: "You can get Session ID in this list.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
tokens: [
|
||||||
|
{
|
||||||
|
title: "Token List",
|
||||||
|
description: "Casdoor is based on OAuth. Tokens are users' OAuth token.You can get access token in this list.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
enforcers: [
|
||||||
|
{
|
||||||
|
title: "Enforcer List",
|
||||||
|
description: "In addition to the API interface for requesting enforcement of permission control, Casdoor also provides other interfaces that help external applications obtain permission policy information, which is also listed here.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
adapters: [
|
||||||
|
{
|
||||||
|
title: "Adapter List",
|
||||||
|
description: "Casdoor supports using the UI to connect the adapter and manage the policy rules. In Casbin, the policy storage is implemented as an adapter (aka middleware for Casbin). A Casbin user can use an adapter to load policy rules from a storage, or save policy rules to it.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
models: [
|
||||||
|
{
|
||||||
|
title: "Model List",
|
||||||
|
description: "Model defines your permission policy structure, and how requests should match these permission policies and their effects. Then you can user model in Permission.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
permissions: [
|
||||||
|
{
|
||||||
|
title: "Permission List",
|
||||||
|
description: "All users associated with a single Casdoor organization are shared between the organization's applications and therefore have access to the applications. Sometimes you may want to restrict users' access to certain applications, or certain resources in a certain application. In this case, you can use Permission implemented by Casbin.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Permission Add",
|
||||||
|
description: "In the Casdoor Web UI, you can add a Model for your organization in the Model configuration item, and a Policy for your organization in the Permission configuration item. ",
|
||||||
|
id: "add-button",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Permission Upload",
|
||||||
|
description: "With Casbin Online Editor, you can get Model and Policy files suitable for your usage scenarios. You can easily import the Model file into Casdoor through the Casdoor Web UI for use by the built-in Casbin. ",
|
||||||
|
id: "upload-button",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
roles: [
|
||||||
|
{
|
||||||
|
title: "Role List",
|
||||||
|
description: "Each user may have multiple roles. You can see the user's roles on the user's profile.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resources: [
|
||||||
|
{
|
||||||
|
title: "Resource List",
|
||||||
|
description: "You can upload resources in casdoor. Before upload resources, you need to configure a storage provider. Please see Storage Provider.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Upload Resource",
|
||||||
|
description: "Users can upload resources such as files and images to the previously configured cloud storage.",
|
||||||
|
id: "upload-button",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
title: "Provider List",
|
||||||
|
description: "We have 6 kinds of providers:OAuth providers、SMS Providers、Email Providers、Storage Providers、Payment Provider、Captcha Provider.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Provider Add",
|
||||||
|
description: "You must add the provider to application, then you can use the provider in your application",
|
||||||
|
id: "add-button",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
organizations: [
|
||||||
|
{
|
||||||
|
title: "Organization List",
|
||||||
|
description: "Organization is the basic unit of Casdoor, which manages users and applications. If a user signed in to an organization, then he can access all applications belonging to the organization without signing in again.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
title: "Group List",
|
||||||
|
description: "In the groups list pages, you can see all the groups in organizations.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
users: [
|
||||||
|
{
|
||||||
|
title: "User List",
|
||||||
|
description: "As an authentication platform, Casdoor is able to manage users.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Import users",
|
||||||
|
description: "You can add new users or update existing Casdoor users by uploading a XLSX file of user information.",
|
||||||
|
id: "upload-button",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
applications: [
|
||||||
|
{
|
||||||
|
title: "Application List",
|
||||||
|
description: "If you want to use Casdoor to provide login service for your web Web APPs, you can add them as Casdoor applications. Users can access all applications in their organizations without login twice.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TourUrlList = ["home", "organizations", "groups", "users", "applications", "providers", "resources", "roles", "permissions", "models", "adapters", "enforcers", "tokens", "sessions", "products", "payments", "plans", "pricings", "subscriptions", "sysinfo", "syncers", "webhooks"];
|
||||||
|
|
||||||
|
export function getNextUrl(pathName = window.location.pathname) {
|
||||||
|
return TourUrlList[TourUrlList.indexOf(pathName.replace("/", "")) + 1] || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setIsTourVisible(visible) {
|
||||||
|
localStorage.setItem("isTourVisible", visible);
|
||||||
|
window.dispatchEvent(new Event("storageTourChanged"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTourVisible() {
|
||||||
|
return localStorage.getItem("isTourVisible") !== "false";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNextButtonChild(nextPathName) {
|
||||||
|
return nextPathName !== "" ?
|
||||||
|
`Go to "${nextPathName.charAt(0).toUpperCase()}${nextPathName.slice(1)} List"`
|
||||||
|
: "Finish";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSteps() {
|
||||||
|
const path = window.location.pathname.replace("/", "");
|
||||||
|
const res = TourObj[path];
|
||||||
|
if (res === undefined) {
|
||||||
|
return [];
|
||||||
|
} else {
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
@@ -187,7 +187,7 @@ class UserListPage extends BaseListPage {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Upload {...props}>
|
<Upload {...props}>
|
||||||
<Button type="primary" size="small">
|
<Button id="upload-button" type="primary" size="small">
|
||||||
<UploadOutlined /> {i18next.t("user:Upload (.xlsx)")}
|
<UploadOutlined /> {i18next.t("user:Upload (.xlsx)")}
|
||||||
</Button>
|
</Button>
|
||||||
</Upload>
|
</Upload>
|
||||||
|
@@ -159,6 +159,17 @@ class WebhookEditPage extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getApiPaths() {
|
||||||
|
const objects = ["organization", "group", "user", "application", "provider", "resource", "cert", "role", "permission", "model", "adapter", "enforcer", "session", "record", "token", "product", "payment", "plan", "pricing", "subscription", "syncer", "webhook"];
|
||||||
|
const res = [];
|
||||||
|
objects.forEach(obj => {
|
||||||
|
["add", "update", "delete"].forEach(action => {
|
||||||
|
res.push(`${action}-${obj}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
renderWebhook() {
|
renderWebhook() {
|
||||||
const preview = Setting.deepCopy(previewTemplate);
|
const preview = Setting.deepCopy(previewTemplate);
|
||||||
if (this.state.webhook.isUserExtended) {
|
if (this.state.webhook.isUserExtended) {
|
||||||
@@ -263,7 +274,7 @@ class WebhookEditPage extends React.Component {
|
|||||||
}} >
|
}} >
|
||||||
{
|
{
|
||||||
(
|
(
|
||||||
["signup", "login", "logout", "add-user", "update-user", "delete-user", "add-organization", "update-organization", "delete-organization", "add-application", "update-application", "delete-application", "add-provider", "update-provider", "delete-provider", "update-subscription"].map((option, index) => {
|
["signup", "login", "logout"].concat(this.getApiPaths()).map((option, index) => {
|
||||||
return (
|
return (
|
||||||
<Option key={option} value={option}>{option}</Option>
|
<Option key={option} value={option}>{option}</Option>
|
||||||
);
|
);
|
||||||
|
@@ -179,7 +179,6 @@ class SignupPage extends React.Component {
|
|||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
values.plan = params.get("plan");
|
values.plan = params.get("plan");
|
||||||
values.pricing = params.get("pricing");
|
values.pricing = params.get("pricing");
|
||||||
|
|
||||||
AuthBackend.signup(values)
|
AuthBackend.signup(values)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === "ok") {
|
if (res.status === "ok") {
|
||||||
|
@@ -14,8 +14,8 @@
|
|||||||
|
|
||||||
import * as Setting from "../Setting";
|
import * as Setting from "../Setting";
|
||||||
|
|
||||||
export function getPayments(owner, organization, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
export function getPayments(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
|
||||||
return fetch(`${Setting.ServerUrl}/api/get-payments?owner=${owner}&organization=${organization}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
return fetch(`${Setting.ServerUrl}/api/get-payments?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
headers: {
|
headers: {
|
||||||
|
@@ -37,7 +37,7 @@ const AppListPage = (props) => {
|
|||||||
return applications.map(application => {
|
return applications.map(application => {
|
||||||
let homepageUrl = application.homepageUrl;
|
let homepageUrl = application.homepageUrl;
|
||||||
if (homepageUrl === "<custom-url>") {
|
if (homepageUrl === "<custom-url>") {
|
||||||
homepageUrl = this.props.account.homepage;
|
homepageUrl = props.account.homepage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@@ -13,15 +13,23 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import {ArrowUpOutlined} from "@ant-design/icons";
|
import {ArrowUpOutlined} from "@ant-design/icons";
|
||||||
import {Card, Col, Row, Statistic} from "antd";
|
import {Card, Col, Row, Statistic, Tour} from "antd";
|
||||||
import * as echarts from "echarts";
|
import * as echarts from "echarts";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import * as DashboardBackend from "../backend/DashboardBackend";
|
import * as DashboardBackend from "../backend/DashboardBackend";
|
||||||
import * as Setting from "../Setting";
|
import * as Setting from "../Setting";
|
||||||
|
import * as TourConfig from "../TourConfig";
|
||||||
|
|
||||||
const Dashboard = (props) => {
|
const Dashboard = (props) => {
|
||||||
const [dashboardData, setDashboardData] = React.useState(null);
|
const [dashboardData, setDashboardData] = React.useState(null);
|
||||||
|
const [isTourVisible, setIsTourVisible] = React.useState(TourConfig.getTourVisible());
|
||||||
|
const nextPathName = TourConfig.getNextUrl("home");
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
window.addEventListener("storageTourChanged", handleTourChange);
|
||||||
|
return () => window.removeEventListener("storageTourChanged", handleTourChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!Setting.isLocalAdminUser(props.account)) {
|
if (!Setting.isLocalAdminUser(props.account)) {
|
||||||
@@ -42,6 +50,35 @@ const Dashboard = (props) => {
|
|||||||
});
|
});
|
||||||
}, [props.owner]);
|
}, [props.owner]);
|
||||||
|
|
||||||
|
const handleTourChange = () => {
|
||||||
|
setIsTourVisible(TourConfig.getTourVisible());
|
||||||
|
};
|
||||||
|
|
||||||
|
const setIsTourToLocal = () => {
|
||||||
|
TourConfig.setIsTourVisible(false);
|
||||||
|
setIsTourVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTourComplete = () => {
|
||||||
|
if (nextPathName !== "") {
|
||||||
|
props.history.push("/" + nextPathName);
|
||||||
|
TourConfig.setIsTourVisible(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSteps = () => {
|
||||||
|
const steps = TourConfig.TourObj["home"];
|
||||||
|
steps.map((item, index) => {
|
||||||
|
item.target = () => document.getElementById(item.id) || null;
|
||||||
|
if (index === steps.length - 1) {
|
||||||
|
item.nextButtonProps = {
|
||||||
|
children: TourConfig.getNextButtonChild(nextPathName),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return steps;
|
||||||
|
};
|
||||||
|
|
||||||
const renderEChart = () => {
|
const renderEChart = () => {
|
||||||
if (dashboardData === null) {
|
if (dashboardData === null) {
|
||||||
return;
|
return;
|
||||||
@@ -83,7 +120,7 @@ const Dashboard = (props) => {
|
|||||||
myChart.setOption(option);
|
myChart.setOption(option);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row gutter={80}>
|
<Row id="statistic" gutter={80}>
|
||||||
<Col span={50}>
|
<Col span={50}>
|
||||||
<Card bordered={false} bodyStyle={{width: "100%", height: "150px", display: "flex", alignItems: "center", justifyContent: "center"}}>
|
<Card bordered={false} bodyStyle={{width: "100%", height: "150px", display: "flex", alignItems: "center", justifyContent: "center"}}>
|
||||||
<Statistic title={i18next.t("home:Total users")} fontSize="100px" value={dashboardData.userCounts[30]} valueStyle={{fontSize: "30px"}} style={{width: "200px", paddingLeft: "10px"}} />
|
<Statistic title={i18next.t("home:Total users")} fontSize="100px" value={dashboardData.userCounts[30]} valueStyle={{fontSize: "30px"}} style={{width: "200px", paddingLeft: "10px"}} />
|
||||||
@@ -112,6 +149,17 @@ const Dashboard = (props) => {
|
|||||||
<div style={{display: "flex", justifyContent: "center", flexDirection: "column", alignItems: "center"}}>
|
<div style={{display: "flex", justifyContent: "center", flexDirection: "column", alignItems: "center"}}>
|
||||||
{renderEChart()}
|
{renderEChart()}
|
||||||
<div id="echarts-chart" style={{width: "80%", height: "400px", textAlign: "center", marginTop: "20px"}} />
|
<div id="echarts-chart" style={{width: "80%", height: "400px", textAlign: "center", marginTop: "20px"}} />
|
||||||
|
<Tour
|
||||||
|
open={isTourVisible}
|
||||||
|
onClose={setIsTourToLocal}
|
||||||
|
steps={getSteps()}
|
||||||
|
indicatorsRender={(current, total) => (
|
||||||
|
<span>
|
||||||
|
{current + 1} / {total}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
onFinish={handleTourComplete}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
50
web/src/common/OpenTour.js
Normal file
50
web/src/common/OpenTour.js
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Copyright 2023 The Casdoor Authors. All Rights Reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import {Tooltip} from "antd";
|
||||||
|
import {QuestionCircleOutlined} from "@ant-design/icons";
|
||||||
|
import * as TourConfig from "../TourConfig";
|
||||||
|
import * as Setting from "../Setting";
|
||||||
|
|
||||||
|
class OpenTour extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isTourVisible: props.isTourVisible ?? TourConfig.getTourVisible(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
canTour = () => {
|
||||||
|
const path = window.location.pathname.replace("/", "");
|
||||||
|
return TourConfig.TourUrlList.indexOf(path) !== -1 || path === "";
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
this.canTour() ?
|
||||||
|
<Tooltip title="Click to enable the help wizard.">
|
||||||
|
<div className="select-box" style={{display: Setting.isMobile() ? "none" : null, ...this.props.style}} onClick={() => TourConfig.setIsTourVisible(true)} >
|
||||||
|
<QuestionCircleOutlined style={{fontSize: "24px", color: "#4d4d4d"}} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
:
|
||||||
|
<div className="select-box" style={{display: Setting.isMobile() ? "none" : null, cursor: "not-allowed", ...this.props.style}} >
|
||||||
|
<QuestionCircleOutlined style={{fontSize: "24px", color: "#adadad"}} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OpenTour;
|
@@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Card, Col, Row} from "antd";
|
import {Card, Col, Radio, Row} from "antd";
|
||||||
import * as PricingBackend from "../backend/PricingBackend";
|
import * as PricingBackend from "../backend/PricingBackend";
|
||||||
import * as PlanBackend from "../backend/PlanBackend";
|
import * as PlanBackend from "../backend/PlanBackend";
|
||||||
import CustomGithubCorner from "../common/CustomGithubCorner";
|
import CustomGithubCorner from "../common/CustomGithubCorner";
|
||||||
@@ -33,6 +33,8 @@ class PricingPage extends React.Component {
|
|||||||
userName: params.get("user"),
|
userName: params.get("user"),
|
||||||
pricing: props.pricing,
|
pricing: props.pricing,
|
||||||
plans: null,
|
plans: null,
|
||||||
|
periods: null,
|
||||||
|
selectedPeriod: null,
|
||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -73,8 +75,12 @@ class PricingPage extends React.Component {
|
|||||||
Setting.showMessage("error", i18next.t("pricing:Failed to get plans"));
|
Setting.showMessage("error", i18next.t("pricing:Failed to get plans"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const plans = results.map(result => result.data);
|
||||||
|
const periods = [... new Set(plans.map(plan => plan.period).filter(period => period !== ""))];
|
||||||
this.setState({
|
this.setState({
|
||||||
plans: results.map(result => result.data),
|
plans: plans,
|
||||||
|
periods: periods,
|
||||||
|
selectedPeriod: periods?.[0],
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -84,10 +90,9 @@ class PricingPage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loadPricing(pricingName) {
|
loadPricing(pricingName) {
|
||||||
if (pricingName === undefined) {
|
if (!pricingName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PricingBackend.getPricing(this.state.owner, pricingName)
|
PricingBackend.getPricing(this.state.owner, pricingName)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res.status === "error") {
|
if (res.status === "error") {
|
||||||
@@ -106,8 +111,31 @@ class PricingPage extends React.Component {
|
|||||||
this.props.onUpdatePricing(pricing);
|
this.props.onUpdatePricing(pricing);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderCards() {
|
renderSelectPeriod() {
|
||||||
|
if (!this.state.periods || this.state.periods.length <= 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Radio.Group
|
||||||
|
value={this.state.selectedPeriod}
|
||||||
|
size="large"
|
||||||
|
buttonStyle="solid"
|
||||||
|
onChange={e => {
|
||||||
|
this.setState({selectedPeriod: e.target.value});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
this.state.periods.map(period => {
|
||||||
|
return (
|
||||||
|
<Radio.Button key={period} value={period}>{period}</Radio.Button>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Radio.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCards() {
|
||||||
const getUrlByPlan = (planName) => {
|
const getUrlByPlan = (planName) => {
|
||||||
const pricing = this.state.pricing;
|
const pricing = this.state.pricing;
|
||||||
let signUpUrl = `/signup/${pricing.application}?plan=${planName}&pricing=${pricing.name}`;
|
let signUpUrl = `/signup/${pricing.application}?plan=${planName}&pricing=${pricing.name}`;
|
||||||
@@ -122,9 +150,9 @@ class PricingPage extends React.Component {
|
|||||||
<Card style={{border: "none"}} bodyStyle={{padding: 0}}>
|
<Card style={{border: "none"}} bodyStyle={{padding: 0}}>
|
||||||
{
|
{
|
||||||
this.state.plans.map(item => {
|
this.state.plans.map(item => {
|
||||||
return (
|
return item.period === this.state.selectedPeriod ? (
|
||||||
<SingleCard link={getUrlByPlan(item.name)} key={item.name} plan={item} isSingle={this.state.plans.length === 1} />
|
<SingleCard link={getUrlByPlan(item.name)} key={item.name} plan={item} isSingle={this.state.plans.length === 1} />
|
||||||
);
|
) : null;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</Card>
|
</Card>
|
||||||
@@ -135,9 +163,9 @@ class PricingPage extends React.Component {
|
|||||||
<Row style={{justifyContent: "center"}} gutter={24}>
|
<Row style={{justifyContent: "center"}} gutter={24}>
|
||||||
{
|
{
|
||||||
this.state.plans.map(item => {
|
this.state.plans.map(item => {
|
||||||
return (
|
return item.period === this.state.selectedPeriod ? (
|
||||||
<SingleCard style={{marginRight: "5px", marginLeft: "5px"}} link={getUrlByPlan(item.name)} key={item.name} plan={item} isSingle={this.state.plans.length === 1} />
|
<SingleCard style={{marginRight: "5px", marginLeft: "5px"}} link={getUrlByPlan(item.name)} key={item.name} plan={item} isSingle={this.state.plans.length === 1} />
|
||||||
);
|
) : null;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</Row>
|
</Row>
|
||||||
@@ -161,6 +189,13 @@ class PricingPage extends React.Component {
|
|||||||
<div className="login-form">
|
<div className="login-form">
|
||||||
<h1 style={{fontSize: "48px", marginTop: "0px", marginBottom: "15px"}}>{pricing.displayName}</h1>
|
<h1 style={{fontSize: "48px", marginTop: "0px", marginBottom: "15px"}}>{pricing.displayName}</h1>
|
||||||
<span style={{fontSize: "20px"}}>{pricing.description}</span>
|
<span style={{fontSize: "20px"}}>{pricing.description}</span>
|
||||||
|
<Row style={{width: "100%", marginTop: "40px"}}>
|
||||||
|
<Col span={24} style={{display: "flex", justifyContent: "center"}} >
|
||||||
|
{
|
||||||
|
this.renderSelectPeriod()
|
||||||
|
}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
<Row style={{width: "100%", marginTop: "40px"}}>
|
<Row style={{width: "100%", marginTop: "40px"}}>
|
||||||
<Col span={24} style={{display: "flex", justifyContent: "center"}} >
|
<Col span={24} style={{display: "flex", justifyContent: "center"}} >
|
||||||
{
|
{
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {Button, Card, Col} from "antd";
|
import {Button, Card, Col, Row} from "antd";
|
||||||
import * as Setting from "../Setting";
|
import * as Setting from "../Setting";
|
||||||
import {withRouter} from "react-router-dom";
|
import {withRouter} from "react-router-dom";
|
||||||
|
|
||||||
@@ -37,17 +37,21 @@ class SingleCard extends React.Component {
|
|||||||
style={isSingle ? {width: "320px", height: "100%"} : {width: "100%", height: "100%", paddingTop: "0px"}}
|
style={isSingle ? {width: "320px", height: "100%"} : {width: "100%", height: "100%", paddingTop: "0px"}}
|
||||||
title={<h2>{plan.displayName}</h2>}
|
title={<h2>{plan.displayName}</h2>}
|
||||||
>
|
>
|
||||||
|
<Col>
|
||||||
|
<Row>
|
||||||
<div style={{textAlign: "left"}} className="px-10 mt-5">
|
<div style={{textAlign: "left"}} className="px-10 mt-5">
|
||||||
<span style={{fontSize: "40px", fontWeight: 700}}>{Setting.getCurrencySymbol(plan.currency)} {plan.pricePerMonth}</span>
|
<span style={{fontSize: "40px", fontWeight: 700}}>{Setting.getCurrencySymbol(plan.currency)} {plan.price}</span>
|
||||||
<span style={{fontSize: "18px", fontWeight: 600, color: "gray"}}> {i18next.t("plan:per month")}</span>
|
<span style={{fontSize: "18px", fontWeight: 600, color: "gray"}}> {plan.period === "Yearly" ? i18next.t("plan:per year") : i18next.t("plan:per month")}</span>
|
||||||
</div>
|
</div>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<br />
|
<Row style={{height: "90px", paddingTop: "15px"}}>
|
||||||
<div style={{textAlign: "left", fontSize: "18px"}}>
|
<div style={{textAlign: "left", fontSize: "18px"}}>
|
||||||
<Meta description={plan.description} />
|
<Meta description={plan.description} />
|
||||||
</div>
|
</div>
|
||||||
<br />
|
</Row>
|
||||||
<ul style={{listStyleType: "none", paddingLeft: "0px", textAlign: "left"}}>
|
|
||||||
|
{/* <ul style={{listStyleType: "none", paddingLeft: "0px", textAlign: "left"}}>
|
||||||
{(plan.options ?? []).map((option) => {
|
{(plan.options ?? []).map((option) => {
|
||||||
// eslint-disable-next-line react/jsx-key
|
// eslint-disable-next-line react/jsx-key
|
||||||
return <li>
|
return <li>
|
||||||
@@ -58,15 +62,16 @@ class SingleCard extends React.Component {
|
|||||||
<span style={{fontSize: "16px"}}>{option}</span>
|
<span style={{fontSize: "16px"}}>{option}</span>
|
||||||
</li>;
|
</li>;
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul> */}
|
||||||
<div style={{minHeight: "60px"}}>
|
|
||||||
|
|
||||||
</div>
|
<Row style={{paddingTop: "15px"}}>
|
||||||
<Button style={{width: "100%", position: "absolute", height: "50px", borderRadius: "0px", bottom: "0", left: "0"}} type="primary" key="subscribe" onClick={() => window.location.href = link}>
|
<Button style={{width: "100%", height: "50px", borderRadius: "0px", bottom: "0", left: "0"}} type="primary" key="subscribe" onClick={() => window.location.href = link}>
|
||||||
{
|
{
|
||||||
i18next.t("pricing:Getting started")
|
i18next.t("pricing:Getting started")
|
||||||
}
|
}
|
||||||
</Button>
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
);
|
);
|
||||||
|
@@ -137,7 +137,7 @@ class SignupTable extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch checked={text} onChange={checked => {
|
<Switch checked={text} disabled={record.name === "Password"} onChange={checked => {
|
||||||
this.updateField(table, index, "required", checked);
|
this.updateField(table, index, "required", checked);
|
||||||
}} />
|
}} />
|
||||||
);
|
);
|
||||||
|
@@ -96,9 +96,9 @@ class SyncerTableColumnTable extends React.Component {
|
|||||||
key: "casdoorName",
|
key: "casdoorName",
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
return (
|
return (
|
||||||
<Select virtual={false} style={{width: "100%"}} value={text} onChange={(value => {this.updateField(table, index, "casdoorName", value);})}>
|
<Select virtual={false} showSearch style={{width: "100%"}} value={text} onChange={(value => {this.updateField(table, index, "casdoorName", value);})}>
|
||||||
{
|
{
|
||||||
["Name", "CreatedTime", "UpdatedTime", "Id", "Type", "Password", "PasswordSalt", "DisplayName", "FirstName", "LastName", "Avatar", "PermanentAvatar",
|
["Owner", "Name", "CreatedTime", "UpdatedTime", "Id", "Type", "Password", "PasswordSalt", "DisplayName", "FirstName", "LastName", "Avatar", "PermanentAvatar",
|
||||||
"Email", "EmailVerified", "Phone", "Location", "Address", "Affiliation", "Title", "IdCardType", "IdCard", "Homepage", "Bio", "Tag", "Region",
|
"Email", "EmailVerified", "Phone", "Location", "Address", "Affiliation", "Title", "IdCardType", "IdCard", "Homepage", "Bio", "Tag", "Region",
|
||||||
"Language", "Gender", "Birthday", "Education", "Score", "Ranking", "IsDefaultAvatar", "IsOnline", "IsAdmin", "IsForbidden", "IsDeleted", "CreatedIp",
|
"Language", "Gender", "Birthday", "Education", "Score", "Ranking", "IsDefaultAvatar", "IsOnline", "IsAdmin", "IsForbidden", "IsDeleted", "CreatedIp",
|
||||||
"PreferredMfaType", "TotpSecret", "SignupApplication"]
|
"PreferredMfaType", "TotpSecret", "SignupApplication"]
|
||||||
|
Reference in New Issue
Block a user