-
+
+
this.renderHomeIfLoggedIn()} />
this.renderHomeIfLoggedIn()} />
@@ -85,6 +102,7 @@ class EntryPage extends React.Component {
this.renderHomeIfLoggedIn()} />
this.renderHomeIfLoggedIn()} />
{return ();}} />
+ this.renderHomeIfLoggedIn()} />
);
diff --git a/web/src/PlanEditPage.js b/web/src/PlanEditPage.js
new file mode 100644
index 00000000..86541b61
--- /dev/null
+++ b/web/src/PlanEditPage.js
@@ -0,0 +1,273 @@
+// 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 {Button, Card, Col, Input, InputNumber, Row, Select, Switch} from "antd";
+import * as OrganizationBackend from "./backend/OrganizationBackend";
+import * as RoleBackend from "./backend/RoleBackend";
+import * as PlanBackend from "./backend/PlanBackend";
+import * as UserBackend from "./backend/UserBackend";
+import * as Setting from "./Setting";
+import i18next from "i18next";
+
+const {Option} = Select;
+
+class PlanEditPage extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ classes: props,
+ organizationName: props.organizationName !== undefined ? props.organizationName : props.match.params.organizationName,
+ planName: props.match.params.planName,
+ plan: null,
+ organizations: [],
+ users: [],
+ roles: [],
+ providers: [],
+ mode: props.location.mode !== undefined ? props.location.mode : "edit",
+ };
+ }
+
+ UNSAFE_componentWillMount() {
+ this.getPlan();
+ this.getOrganizations();
+ }
+
+ getPlan() {
+ PlanBackend.getPlan(this.state.organizationName, this.state.planName)
+ .then((plan) => {
+ this.setState({
+ plan: plan,
+ });
+
+ this.getUsers(plan.owner);
+ this.getRoles(plan.owner);
+ });
+ }
+
+ getRoles(organizationName) {
+ RoleBackend.getRoles(organizationName)
+ .then((res) => {
+ this.setState({
+ roles: res,
+ });
+ });
+ }
+
+ getUsers(organizationName) {
+ UserBackend.getUsers(organizationName)
+ .then((res) => {
+ this.setState({
+ users: res,
+ });
+ });
+ }
+
+ getOrganizations() {
+ OrganizationBackend.getOrganizations("admin")
+ .then((res) => {
+ this.setState({
+ organizations: (res.msg === undefined) ? res : [],
+ });
+ });
+ }
+
+ parsePlanField(key, value) {
+ if ([""].includes(key)) {
+ value = Setting.myParseInt(value);
+ }
+ return value;
+ }
+
+ updatePlanField(key, value) {
+ value = this.parsePlanField(key, value);
+
+ const plan = this.state.plan;
+ plan[key] = value;
+ this.setState({
+ plan: plan,
+ });
+ }
+
+ renderPlan() {
+ return (
+
+ {this.state.mode === "add" ? i18next.t("plan:New Plan") : i18next.t("plan:Edit Plan")}
+
+
+ {this.state.mode === "add" ? : null}
+
+ } style={(Setting.isMobile()) ? {margin: "5px"} : {}} type="inner">
+
+ {
+ this.state.plan !== null ? this.renderPlan() : null
+ }
+
+
+
+ {this.state.mode === "add" ? : null}
+
+
+ );
+ }
+}
+
+export default PlanEditPage;
diff --git a/web/src/PlanListPage.js b/web/src/PlanListPage.js
new file mode 100644
index 00000000..049c81ac
--- /dev/null
+++ b/web/src/PlanListPage.js
@@ -0,0 +1,236 @@
+// 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 {Link} from "react-router-dom";
+import {Button, Switch, Table} from "antd";
+import moment from "moment";
+import * as Setting from "./Setting";
+import * as PlanBackend from "./backend/PlanBackend";
+import i18next from "i18next";
+import BaseListPage from "./BaseListPage";
+import PopconfirmModal from "./common/modal/PopconfirmModal";
+
+class PlanListPage extends BaseListPage {
+ newPlan() {
+ const randomName = Setting.getRandomName();
+ const owner = (this.state.organizationName !== undefined) ? this.state.organizationName : this.props.account.owner;
+
+ return {
+ owner: owner,
+ name: `plan_${randomName}`,
+ createdTime: moment().format(),
+ pricePerMonth: 10,
+ pricePerYear: 100,
+ currency: "USD",
+ displayName: `New Plan - ${randomName}`,
+ };
+ }
+
+ addPlan() {
+ const newPlan = this.newPlan();
+ PlanBackend.addPlan(newPlan)
+ .then((res) => {
+ if (res.status === "ok") {
+ this.props.history.push({pathname: `/plan/${newPlan.owner}/${newPlan.name}`, mode: "add"});
+ Setting.showMessage("success", i18next.t("general:Successfully added"));
+ } else {
+ Setting.showMessage("error", `${i18next.t("general:Failed to add")}: ${res.msg}`);
+ }
+ })
+ .catch(error => {
+ Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
+ });
+ }
+
+ deletePlan(i) {
+ PlanBackend.deletePlan(this.state.data[i])
+ .then((res) => {
+ if (res.status === "ok") {
+ Setting.showMessage("success", i18next.t("general:Successfully deleted"));
+ this.setState({
+ data: Setting.deleteRow(this.state.data, i),
+ pagination: {total: this.state.pagination.total - 1},
+ });
+ } else {
+ Setting.showMessage("error", `${i18next.t("general:Failed to delete")}: ${res.msg}`);
+ }
+ })
+ .catch(error => {
+ Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
+ });
+ }
+
+ renderTable(plans) {
+ const columns = [
+ {
+ title: i18next.t("general:Name"),
+ dataIndex: "name",
+ key: "name",
+ width: "140px",
+ fixed: "left",
+ sorter: true,
+ ...this.getColumnSearchProps("name"),
+ render: (text, record, index) => {
+ return (
+