Compare commits

...

8 Commits

Author SHA1 Message Date
Tower He
d3d4646c56 feat: fix can not create db when using pg with a dbname in DSN (#2280)
* fix: can not create db when using pg with a dbname in DSN

* Update ormer.go

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-08-31 18:05:38 +08:00
Yang Luo
669ac7c618 Don't encrypt user pass when user.PasswordType is non-empty when adding users 2023-08-31 17:49:36 +08:00
Yang Luo
6715efd781 Fix enforcer edit page 2023-08-31 17:32:36 +08:00
haiwu
953be4a7b6 feat: support subscription periods (yearly/monthly) (#2265)
* feat: support year/month subscription

* feat: add GetPrice() for plan

* feat: add GetDuration

* feat: gofumpt

* feat: add subscription mode for pricing

* feat: restrict auto create product operation

* fix: format code

* feat: add period for plan,remove period from pricing

* feat: format code

* feat: remove space

* feat: remove period in signup page
2023-08-30 17:13:45 +08:00
Yang Luo
943cc43427 Fix payment list and product edit actions 2023-08-28 21:01:23 +08:00
Yang Luo
1e5ce7a045 Fix crash in syncUsersNoError() 2023-08-28 01:51:06 +08:00
Baihhh
7a85b74573 fix: fix tour disabled state (#2264)
* fix: distinguish between pages that can tour or not

* Update OpenTour.js

---------

Co-authored-by: hsluoyz <hsluoyz@qq.com>
2023-08-27 23:18:14 +08:00
Yang Luo
7e349c1768 feat: fix crash bug in getSteps() 2023-08-27 21:58:58 +08:00
24 changed files with 260 additions and 177 deletions

View File

@@ -83,7 +83,7 @@ func (c *ApiController) GetEnforcer() {
return
}
if loadModelCfg == "true" {
if loadModelCfg == "true" && enforcer.Model != "" {
err := enforcer.LoadModelCfg()
if err != nil {
return

View File

@@ -114,7 +114,7 @@ func (c *ApiController) GetPlan() {
// @router /update-plan [post]
func (c *ApiController) UpdatePlan() {
id := c.Input().Get("id")
owner := util.GetOwnerFromId(id)
var plan object.Plan
err := json.Unmarshal(c.Ctx.Input.RequestBody, &plan)
if err != nil {
@@ -122,17 +122,19 @@ func (c *ApiController) UpdatePlan() {
return
}
if plan.Product != "" {
planId := util.GetId(plan.Owner, plan.Product)
product, err := object.GetProduct(planId)
productId := util.GetId(owner, plan.Product)
product, err := object.GetProduct(productId)
if err != nil {
c.ResponseError(err.Error())
return
}
object.UpdateProductForPlan(&plan, product)
_, err = object.UpdateProduct(planId, product)
if err != nil {
c.ResponseError(err.Error())
return
if product != nil {
object.UpdateProductForPlan(&plan, product)
_, err = object.UpdateProduct(productId, product)
if err != nil {
c.ResponseError(err.Error())
return
}
}
}
c.Data["json"] = wrapActionResponse(object.UpdatePlan(id, &plan))

View File

@@ -18,6 +18,7 @@ import (
"database/sql"
"flag"
"fmt"
"regexp"
"runtime"
"strings"
@@ -122,9 +123,14 @@ func NewAdapter(driverName string, dataSourceName string, dbName string) *Ormer
return a
}
func refineDataSourceNameForPostgres(dataSourceName string) string {
reg := regexp.MustCompile(`dbname=[^ ]+\s*`)
return reg.ReplaceAllString(dataSourceName, "")
}
func createDatabaseForPostgres(driverName string, dataSourceName string, dbName string) error {
if driverName == "postgres" {
db, err := sql.Open(driverName, dataSourceName)
db, err := sql.Open(driverName, refineDataSourceNameForPostgres(dataSourceName))
if err != nil {
return err
}

View File

@@ -16,6 +16,7 @@ package object
import (
"fmt"
"time"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
@@ -28,10 +29,10 @@ type Plan struct {
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Description string `xorm:"varchar(100)" json:"description"`
PricePerMonth float64 `json:"pricePerMonth"`
PricePerYear float64 `json:"pricePerYear"`
Price float64 `json:"price"`
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
IsEnabled bool `json:"isEnabled"`
@@ -39,10 +40,28 @@ type Plan struct {
Options []string `xorm:"-" json:"options"`
}
const (
PeriodMonthly = "Monthly"
PeriodYearly = "Yearly"
)
func (plan *Plan) GetId() string {
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) {
session := GetSession(owner, -1, -1, field, value, "", "")
return session.Count(&Plan{})

View File

@@ -32,12 +32,6 @@ type Pricing struct {
IsEnabled bool `json:"isEnabled"`
TrialDuration int `json:"trialDuration"`
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 {

View File

@@ -190,8 +190,15 @@ func BuyProduct(id string, user *User, providerName, pricingName, planName, host
if user.Type == "paid-user" {
// Create a subscription for `paid-user`
if pricingName != "" && planName != "" {
sub := NewSubscription(owner, user.Name, pricingName, planName, paymentName)
_, err := AddSubscription(sub)
plan, err := GetPlan(util.GetId(owner, planName))
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 {
return "", "", err
}
@@ -267,14 +274,14 @@ func CreateProductForPlan(plan *Plan) *Product {
product := &Product{
Owner: plan.Owner,
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,
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,
Tag: "auto_created_product_for_plan",
Price: plan.PricePerMonth, // TODO
Price: plan.Price,
Currency: plan.Currency,
Quantity: 999,
@@ -290,9 +297,10 @@ func CreateProductForPlan(plan *Plan) *Product {
}
func UpdateProductForPlan(plan *Plan, product *Product) {
product.DisplayName = fmt.Sprintf("Auto Created Product for Plan %v(%v)", plan.GetId(), plan.DisplayName)
product.Detail = fmt.Sprintf("This Product was auto created for Plan %v(%v)", plan.GetId(), plan.DisplayName)
product.Price = plan.PricePerMonth // TODO
product.Providers = plan.PaymentProviders
product.Owner = plan.Owner
product.DisplayName = fmt.Sprintf("Product for Plan %v/%v/%v", plan.Name, plan.DisplayName, plan.Period)
product.Detail = fmt.Sprintf("This product was auto created for plan %v(%v), subscription period is %v", plan.Name, plan.DisplayName, plan.Period)
product.Price = plan.Price
product.Currency = plan.Currency
product.Providers = plan.PaymentProviders
}

View File

@@ -50,7 +50,7 @@ type Subscription struct {
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
Duration int `json:"duration"`
Period string `xorm:"varchar(100)" json:"period"`
State SubscriptionState `xorm:"varchar(100)" json:"state"`
}
@@ -103,7 +103,8 @@ func (sub *Subscription) UpdateState() error {
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]
return &Subscription{
Owner: owner,
@@ -112,13 +113,12 @@ func NewSubscription(owner, userName, pricingName, planName, paymentName string)
CreatedTime: util.GetCurrentTime(),
User: userName,
Pricing: pricingName,
Plan: planName,
Payment: paymentName,
StartTime: time.Now(),
EndTime: time.Now().AddDate(0, 0, 30),
Duration: 30, // TODO
StartTime: startTime,
EndTime: endTime,
Period: period,
State: SubStatePending, // waiting for payment complete
}
}

View File

@@ -148,6 +148,6 @@ func (syncer *Syncer) syncUsers() error {
func (syncer *Syncer) syncUsersNoError() {
err := syncer.syncUsers()
if err != nil {
panic(err)
fmt.Printf("syncUsersNoError() error: %s\n", err.Error())
}
}

View File

@@ -627,12 +627,10 @@ func AddUser(user *User) (bool, error) {
return false, nil
}
if user.PasswordType == "" && organization.PasswordType != "" {
user.PasswordType = organization.PasswordType
if user.PasswordType == "" || user.PasswordType == "plain" {
user.UpdateUserPassword(organization)
}
user.UpdateUserPassword(organization)
err = user.UpdateUserHash()
if err != nil {
return false, err

View File

@@ -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"),
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"),
dataIndex: "user",
@@ -264,7 +264,7 @@ class PaymentListPage extends BaseListPage {
value = params.type;
}
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) => {
this.setState({
loading: false,

View File

@@ -197,22 +197,29 @@ class PlanEditPage extends React.Component {
</Row>
<Row style={{marginTop: "20px"}} >
<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 span={22} >
<InputNumber value={this.state.plan.pricePerMonth} onChange={value => {
this.updatePlanField("pricePerMonth", value);
<InputNumber value={this.state.plan.price} onChange={value => {
this.updatePlanField("price", value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<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 span={22} >
<InputNumber value={this.state.plan.pricePerYear} onChange={value => {
this.updatePlanField("pricePerYear", value);
}} />
<Select
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>
</Row>
<Row style={{marginTop: "20px"}} >

View File

@@ -32,9 +32,9 @@ class PlanListPage extends BaseListPage {
createdTime: moment().format(),
displayName: `New Plan - ${randomName}`,
description: "",
pricePerMonth: 10,
pricePerYear: 100,
price: 10,
currency: "USD",
period: "Monthly",
isEnabled: true,
paymentProviders: [],
role: "",
@@ -136,18 +136,18 @@ class PlanListPage extends BaseListPage {
...this.getColumnSearchProps("currency"),
},
{
title: i18next.t("plan:Price per month"),
dataIndex: "pricePerMonth",
key: "pricePerMonth",
title: i18next.t("plan:Price"),
dataIndex: "price",
key: "price",
width: "130px",
...this.getColumnSearchProps("pricePerMonth"),
...this.getColumnSearchProps("price"),
},
{
title: i18next.t("plan:Price per year"),
dataIndex: "pricePerYear",
key: "pricePerYear",
title: i18next.t("plan:Period"),
dataIndex: "period",
key: "period",
width: "130px",
...this.getColumnSearchProps("pricePerYear"),
...this.getColumnSearchProps("period"),
},
{
title: i18next.t("general:Role"),

View File

@@ -76,11 +76,10 @@ class ProductBuyPage extends React.Component {
throw new Error(res.msg);
}
const plan = res.data;
const productName = plan.product;
await this.setStateAsync({
pricing: pricing,
plan: plan,
productName: productName,
productName: plan.product,
});
this.onUpdatePricing(pricing);
}

View File

@@ -98,6 +98,7 @@ class ProductEditPage extends React.Component {
}
renderProduct() {
const isCreatedByPlan = this.state.product.tag === "auto_created_product_for_plan";
return (
<Card size="small" title={
<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}
</div>
} 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"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Name"), i18next.t("general:Name - Tooltip"))} :
</Col>
<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);
}} />
</Col>
@@ -127,18 +140,6 @@ class ProductEditPage extends React.Component {
}} />
</Col>
</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"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{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"))} :
</Col>
<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);
}} />
</Col>
@@ -201,7 +202,7 @@ class ProductEditPage extends React.Component {
{Setting.getLabel(i18next.t("payment:Currency"), i18next.t("payment:Currency - Tooltip"))} :
</Col>
<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);
})}>
{
@@ -218,7 +219,7 @@ class ProductEditPage extends React.Component {
{Setting.getLabel(i18next.t("product:Price"), i18next.t("product:Price - Tooltip"))} :
</Col>
<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);
}} />
</Col>
@@ -228,7 +229,7 @@ class ProductEditPage extends React.Component {
{Setting.getLabel(i18next.t("product:Quantity"), i18next.t("product:Quantity - Tooltip"))} :
</Col>
<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);
}} />
</Col>
@@ -238,7 +239,7 @@ class ProductEditPage extends React.Component {
{Setting.getLabel(i18next.t("product:Sold"), i18next.t("product:Sold - Tooltip"))} :
</Col>
<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);
}} />
</Col>
@@ -248,7 +249,7 @@ class ProductEditPage extends React.Component {
{Setting.getLabel(i18next.t("product:Payment providers"), i18next.t("product:Payment providers - Tooltip"))} :
</Col>
<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>)
}
@@ -312,7 +313,7 @@ class ProductEditPage extends React.Component {
submitProductEdit(willExist) {
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) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully saved"));

View File

@@ -253,11 +253,13 @@ class ProductListPage extends BaseListPage {
width: "230px",
fixed: (Setting.isMobile()) ? "false" : "right",
render: (text, record, index) => {
const isCreatedByPlan = record.tag === "auto_created_product_for_plan";
return (
<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"}} type="primary" onClick={() => this.props.history.push(`/products/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal
disabled={isCreatedByPlan}
title={i18next.t("general:Sure to delete") + `: ${record.name} ?`}
onConfirm={() => this.deleteProduct(index)}
>

View File

@@ -14,7 +14,7 @@
import moment from "moment";
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 PricingBackend from "./backend/PricingBackend";
import * as PlanBackend from "./backend/PlanBackend";
@@ -171,16 +171,6 @@ class SubscriptionEditPage extends React.Component {
}} />
</Col>
</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"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("subscription:Start time"), i18next.t("subscription:Start time - Tooltip"))}
@@ -201,6 +191,23 @@ class SubscriptionEditPage extends React.Component {
}} />
</Col>
</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"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:User"), i18next.t("general:User - Tooltip"))} :

View File

@@ -27,7 +27,6 @@ class SubscriptionListPage extends BaseListPage {
newSubscription() {
const randomName = Setting.getRandomName();
const owner = Setting.getRequestOrganization(this.props.account);
const defaultDuration = 30;
return {
owner: owner,
@@ -35,8 +34,8 @@ class SubscriptionListPage extends BaseListPage {
createdTime: moment().format(),
displayName: `New Subscription - ${randomName}`,
startTime: moment().format(),
endTime: moment().add(defaultDuration, "d").format(),
duration: defaultDuration,
endTime: moment().add(30, "d").format(),
period: "Monthly",
description: "",
user: "",
plan: "",
@@ -130,11 +129,11 @@ class SubscriptionListPage extends BaseListPage {
...this.getColumnSearchProps("displayName"),
},
{
title: i18next.t("subscription:Duration"),
dataIndex: "duration",
key: "duration",
title: i18next.t("subscription:Period"),
dataIndex: "period",
key: "period",
width: "140px",
...this.getColumnSearchProps("duration"),
...this.getColumnSearchProps("period"),
},
{
title: i18next.t("subscription:Start time"),
@@ -150,20 +149,6 @@ class SubscriptionListPage extends BaseListPage {
width: "140px",
...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"),
dataIndex: "plan",

View File

@@ -218,6 +218,12 @@ export function getNextButtonChild(nextPathName) {
: "Finish";
}
export function getSteps(pathName = window.location.pathname) {
return TourObj[(pathName.replace("/", ""))];
export function getSteps() {
const path = window.location.pathname.replace("/", "");
const res = TourObj[path];
if (res === undefined) {
return [];
} else {
return res;
}
}

View File

@@ -179,7 +179,6 @@ class SignupPage extends React.Component {
const params = new URLSearchParams(window.location.search);
values.plan = params.get("plan");
values.pricing = params.get("pricing");
AuthBackend.signup(values)
.then((res) => {
if (res.status === "ok") {

View File

@@ -14,8 +14,8 @@
import * as Setting from "../Setting";
export function getPayments(owner, organization, 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}`, {
export function getPayments(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-payments?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET",
credentials: "include",
headers: {

View File

@@ -26,13 +26,23 @@ class OpenTour extends React.Component {
};
}
canTour = () => {
const path = window.location.pathname.replace("/", "");
return TourConfig.TourUrlList.indexOf(path) !== -1 || path === "";
};
render() {
return (
<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"}} />
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>
</Tooltip>
);
}
}

View File

@@ -13,7 +13,7 @@
// limitations under the License.
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 PlanBackend from "../backend/PlanBackend";
import CustomGithubCorner from "../common/CustomGithubCorner";
@@ -33,6 +33,8 @@ class PricingPage extends React.Component {
userName: params.get("user"),
pricing: props.pricing,
plans: null,
periods: null,
selectedPeriod: null,
loading: false,
};
}
@@ -73,8 +75,12 @@ class PricingPage extends React.Component {
Setting.showMessage("error", i18next.t("pricing:Failed to get plans"));
return;
}
const plans = results.map(result => result.data);
const periods = [... new Set(plans.map(plan => plan.period).filter(period => period !== ""))];
this.setState({
plans: results.map(result => result.data),
plans: plans,
periods: periods,
selectedPeriod: periods?.[0],
loading: false,
});
})
@@ -84,10 +90,9 @@ class PricingPage extends React.Component {
}
loadPricing(pricingName) {
if (pricingName === undefined) {
if (!pricingName) {
return;
}
PricingBackend.getPricing(this.state.owner, pricingName)
.then((res) => {
if (res.status === "error") {
@@ -106,8 +111,31 @@ class PricingPage extends React.Component {
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 pricing = this.state.pricing;
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}}>
{
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} />
);
) : null;
})
}
</Card>
@@ -135,9 +163,9 @@ class PricingPage extends React.Component {
<Row style={{justifyContent: "center"}} gutter={24}>
{
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} />
);
) : null;
})
}
</Row>
@@ -161,6 +189,13 @@ class PricingPage extends React.Component {
<div className="login-form">
<h1 style={{fontSize: "48px", marginTop: "0px", marginBottom: "15px"}}>{pricing.displayName}</h1>
<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"}}>
<Col span={24} style={{display: "flex", justifyContent: "center"}} >
{

View File

@@ -14,7 +14,7 @@
import i18next from "i18next";
import React from "react";
import {Button, Card, Col} from "antd";
import {Button, Card, Col, Row} from "antd";
import * as Setting from "../Setting";
import {withRouter} from "react-router-dom";
@@ -37,36 +37,41 @@ class SingleCard extends React.Component {
style={isSingle ? {width: "320px", height: "100%"} : {width: "100%", height: "100%", paddingTop: "0px"}}
title={<h2>{plan.displayName}</h2>}
>
<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: "18px", fontWeight: 600, color: "gray"}}> {i18next.t("plan:per month")}</span>
</div>
<Col>
<Row>
<div style={{textAlign: "left"}} className="px-10 mt-5">
<span style={{fontSize: "40px", fontWeight: 700}}>{Setting.getCurrencySymbol(plan.currency)} {plan.price}</span>
<span style={{fontSize: "18px", fontWeight: 600, color: "gray"}}> {plan.period === "Yearly" ? i18next.t("plan:per year") : i18next.t("plan:per month")}</span>
</div>
</Row>
<br />
<div style={{textAlign: "left", fontSize: "18px"}}>
<Meta description={plan.description} />
</div>
<br />
<ul style={{listStyleType: "none", paddingLeft: "0px", textAlign: "left"}}>
{(plan.options ?? []).map((option) => {
// eslint-disable-next-line react/jsx-key
return <li>
<svg style={{height: "1rem", width: "1rem", fill: "green", marginRight: "10px"}} xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20">
<path d="M0 11l2-2 5 5L18 3l2 2L7 18z"></path>
</svg>
<span style={{fontSize: "16px"}}>{option}</span>
</li>;
})}
</ul>
<div style={{minHeight: "60px"}}>
<Row style={{height: "90px", paddingTop: "15px"}}>
<div style={{textAlign: "left", fontSize: "18px"}}>
<Meta description={plan.description} />
</div>
</Row>
</div>
<Button style={{width: "100%", position: "absolute", height: "50px", borderRadius: "0px", bottom: "0", left: "0"}} type="primary" key="subscribe" onClick={() => window.location.href = link}>
{
i18next.t("pricing:Getting started")
}
</Button>
{/* <ul style={{listStyleType: "none", paddingLeft: "0px", textAlign: "left"}}>
{(plan.options ?? []).map((option) => {
// eslint-disable-next-line react/jsx-key
return <li>
<svg style={{height: "1rem", width: "1rem", fill: "green", marginRight: "10px"}} xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20">
<path d="M0 11l2-2 5 5L18 3l2 2L7 18z"></path>
</svg>
<span style={{fontSize: "16px"}}>{option}</span>
</li>;
})}
</ul> */}
<Row style={{paddingTop: "15px"}}>
<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")
}
</Button>
</Row>
</Col>
</Card>
</Col>
);

View File

@@ -41,7 +41,7 @@ class PolicyTable extends React.Component {
}
UNSAFE_componentWillMount() {
if (this.props.mode === "edit") {
if (this.props.mode === "edit" && this.props.enforcer.adapter !== "") {
this.getPolicies();
}
}
@@ -186,7 +186,7 @@ class PolicyTable extends React.Component {
];
const columnKeys = ["V0", "V1", "V2", "V3", "V4", "V5"];
const columnTitles = this.props.modelCfg["p"].split(",");
const columnTitles = this.props.modelCfg ? this.props.modelCfg["p"].split(",") : columnKeys;
columnTitles.forEach((title, i) => {
columns.push({
title: title,
@@ -247,7 +247,7 @@ class PolicyTable extends React.Component {
loading={this.state.loading}
title={() => (
<div>
<Button disabled={this.state.editingIndex !== "" || Setting.builtInObject(this.props.enforcer)} style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
<Button disabled={this.state.editingIndex !== "" || this.props.enforcer.adapter === "" || Setting.builtInObject(this.props.enforcer)} style={{marginRight: "5px"}} type="primary" size="small" onClick={() => this.addRow(table)}>{i18next.t("general:Add")}</Button>
</div>
)}
/>
@@ -257,7 +257,7 @@ class PolicyTable extends React.Component {
render() {
return (
<React.Fragment>
<Button style={{marginBottom: "10px", width: "150px"}} type="primary" disabled={this.state.editingIndex !== ""} onClick={() => {this.getPolicies();}}>
<Button disabled={this.state.editingIndex !== "" || this.props.enforcer.adapter === ""} style={{marginBottom: "10px", width: "150px"}} type="primary" onClick={() => {this.getPolicies();}}>
{i18next.t("general:Sync")}
</Button>
{