feat: add Agent list and edit pages (#5338)

This commit is contained in:
Modo
2026-03-30 09:10:18 +08:00
committed by GitHub
parent 5b58d8bf16
commit b690ee4ea3
10 changed files with 800 additions and 0 deletions

149
controllers/agent.go Normal file
View File

@@ -0,0 +1,149 @@
// Copyright 2026 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.
package controllers
import (
"encoding/json"
"github.com/beego/beego/v2/server/web/pagination"
"github.com/casdoor/casdoor/object"
"github.com/casdoor/casdoor/util"
)
// GetAgents
// @Title GetAgents
// @Tag Agent API
// @Description get agents
// @Param owner query string true "The owner of agents"
// @Success 200 {array} object.Agent The Response object
// @router /get-agents [get]
func (c *ApiController) GetAgents() {
owner := c.Ctx.Input.Query("owner")
if owner == "admin" {
owner = ""
}
limit := c.Ctx.Input.Query("pageSize")
page := c.Ctx.Input.Query("p")
field := c.Ctx.Input.Query("field")
value := c.Ctx.Input.Query("value")
sortField := c.Ctx.Input.Query("sortField")
sortOrder := c.Ctx.Input.Query("sortOrder")
if limit == "" || page == "" {
agents, err := object.GetAgents(owner)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(agents)
return
}
limitInt := util.ParseInt(limit)
count, err := object.GetAgentCount(owner, field, value)
if err != nil {
c.ResponseError(err.Error())
return
}
paginator := pagination.SetPaginator(c.Ctx, limitInt, count)
agents, err := object.GetPaginationAgents(owner, paginator.Offset(), limitInt, field, value, sortField, sortOrder)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(agents, paginator.Nums())
}
// GetAgent
// @Title GetAgent
// @Tag Agent API
// @Description get agent
// @Param id query string true "The id ( owner/name ) of the agent"
// @Success 200 {object} object.Agent The Response object
// @router /get-agent [get]
func (c *ApiController) GetAgent() {
id := c.Ctx.Input.Query("id")
agent, err := object.GetAgent(id)
if err != nil {
c.ResponseError(err.Error())
return
}
c.ResponseOk(agent)
}
// UpdateAgent
// @Title UpdateAgent
// @Tag Agent API
// @Description update agent
// @Param id query string true "The id ( owner/name ) of the agent"
// @Param body body object.Agent true "The details of the agent"
// @Success 200 {object} controllers.Response The Response object
// @router /update-agent [post]
func (c *ApiController) UpdateAgent() {
id := c.Ctx.Input.Query("id")
var agent object.Agent
err := json.Unmarshal(c.Ctx.Input.RequestBody, &agent)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.UpdateAgent(id, &agent))
c.ServeJSON()
}
// AddAgent
// @Title AddAgent
// @Tag Agent API
// @Description add agent
// @Param body body object.Agent true "The details of the agent"
// @Success 200 {object} controllers.Response The Response object
// @router /add-agent [post]
func (c *ApiController) AddAgent() {
var agent object.Agent
err := json.Unmarshal(c.Ctx.Input.RequestBody, &agent)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.AddAgent(&agent))
c.ServeJSON()
}
// DeleteAgent
// @Title DeleteAgent
// @Tag Agent API
// @Description delete agent
// @Param body body object.Agent true "The details of the agent"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-agent [post]
func (c *ApiController) DeleteAgent() {
var agent object.Agent
err := json.Unmarshal(c.Ctx.Input.RequestBody, &agent)
if err != nil {
c.ResponseError(err.Error())
return
}
c.Data["json"] = wrapActionResponse(object.DeleteAgent(&agent))
c.ServeJSON()
}

118
object/agent.go Normal file
View File

@@ -0,0 +1,118 @@
// Copyright 2026 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.
package object
import (
"fmt"
"github.com/casdoor/casdoor/util"
"github.com/xorm-io/core"
)
type Agent struct {
Owner string `xorm:"varchar(100) notnull pk" json:"owner"`
Name string `xorm:"varchar(100) notnull pk" json:"name"`
CreatedTime string `xorm:"varchar(100)" json:"createdTime"`
UpdatedTime string `xorm:"varchar(100)" json:"updatedTime"`
DisplayName string `xorm:"varchar(100)" json:"displayName"`
Url string `xorm:"varchar(500)" json:"url"`
Token string `xorm:"varchar(500)" json:"token"`
Application string `xorm:"varchar(100)" json:"application"`
}
func GetAgents(owner string) ([]*Agent, error) {
agents := []*Agent{}
err := ormer.Engine.Desc("created_time").Find(&agents, &Agent{Owner: owner})
if err != nil {
return nil, err
}
return agents, nil
}
func getAgent(owner string, name string) (*Agent, error) {
agent := Agent{Owner: owner, Name: name}
existed, err := ormer.Engine.Get(&agent)
if err != nil {
return nil, err
}
if existed {
return &agent, nil
}
return nil, nil
}
func GetAgent(id string) (*Agent, error) {
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
return getAgent(owner, name)
}
func UpdateAgent(id string, agent *Agent) (bool, error) {
owner, name := util.GetOwnerAndNameFromIdNoCheck(id)
if a, err := getAgent(owner, name); err != nil {
return false, err
} else if a == nil {
return false, nil
}
agent.UpdatedTime = util.GetCurrentTime()
_, err := ormer.Engine.ID(core.PK{owner, name}).AllCols().Update(agent)
if err != nil {
return false, err
}
return true, nil
}
func AddAgent(agent *Agent) (bool, error) {
affected, err := ormer.Engine.Insert(agent)
if err != nil {
return false, err
}
return affected != 0, nil
}
func DeleteAgent(agent *Agent) (bool, error) {
affected, err := ormer.Engine.ID(core.PK{agent.Owner, agent.Name}).Delete(&Agent{})
if err != nil {
return false, err
}
return affected != 0, nil
}
func (agent *Agent) GetId() string {
return fmt.Sprintf("%s/%s", agent.Owner, agent.Name)
}
func GetAgentCount(owner, field, value string) (int64, error) {
session := GetSession(owner, -1, -1, field, value, "", "")
return session.Count(&Agent{})
}
func GetPaginationAgents(owner string, offset, limit int, field, value, sortField, sortOrder string) ([]*Agent, error) {
agents := []*Agent{}
session := GetSession(owner, offset, limit, field, value, sortField, sortOrder)
err := session.Find(&agents)
if err != nil {
return agents, err
}
return agents, nil
}

View File

@@ -477,4 +477,9 @@ func (a *Ormer) createTable() {
if err != nil {
panic(err)
}
err = a.Engine.Sync2(new(Agent))
if err != nil {
panic(err)
}
}

View File

@@ -140,6 +140,12 @@ func InitAPI() {
web.Router("/api/delete-server", &controllers.ApiController{}, "POST:DeleteServer")
web.Router("/api/server/:owner/:name", &controllers.ApiController{}, "POST:ProxyServer")
web.Router("/api/get-agents", &controllers.ApiController{}, "GET:GetAgents")
web.Router("/api/get-agent", &controllers.ApiController{}, "GET:GetAgent")
web.Router("/api/update-agent", &controllers.ApiController{}, "POST:UpdateAgent")
web.Router("/api/add-agent", &controllers.ApiController{}, "POST:AddAgent")
web.Router("/api/delete-agent", &controllers.ApiController{}, "POST:DeleteAgent")
web.Router("/api/get-rules", &controllers.ApiController{}, "GET:GetRules")
web.Router("/api/get-rule", &controllers.ApiController{}, "GET:GetRule")
web.Router("/api/add-rule", &controllers.ApiController{}, "POST:AddRule")

228
web/src/AgentEditPage.js Normal file
View File

@@ -0,0 +1,228 @@
// Copyright 2026 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, Row, Select} from "antd";
import {LinkOutlined} from "@ant-design/icons";
import * as AgentBackend from "./backend/AgentBackend";
import * as Setting from "./Setting";
import i18next from "i18next";
import * as OrganizationBackend from "./backend/OrganizationBackend";
import * as ApplicationBackend from "./backend/ApplicationBackend";
const {Option} = Select;
class AgentEditPage extends React.Component {
constructor(props) {
super(props);
this.state = {
classes: props,
agentName: props.match.params.agentName,
owner: props.match.params.organizationName,
agent: null,
organizations: [],
applications: [],
mode: props.location.mode !== undefined ? props.location.mode : "edit",
};
}
UNSAFE_componentWillMount() {
this.getAgent();
this.getOrganizations();
this.getApplications(this.state.owner);
}
getAgent() {
AgentBackend.getAgent(this.state.agent?.owner || this.state.owner, this.state.agentName)
.then((res) => {
if (res.data === null) {
this.props.history.push("/404");
return;
}
if (res.status === "ok") {
this.setState({
agent: res.data,
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to get")}: ${res.msg}`);
}
});
}
getOrganizations() {
if (Setting.isAdminUser(this.props.account)) {
OrganizationBackend.getOrganizations("admin")
.then((res) => {
this.setState({
organizations: res.data || [],
});
});
}
}
getApplications(owner) {
ApplicationBackend.getApplicationsByOrganization("admin", owner)
.then((res) => {
this.setState({
applications: res.data || [],
});
});
}
updateAgentField(key, value) {
const agent = this.state.agent;
if (key === "owner" && agent.owner !== value) {
agent.application = "";
this.getApplications(value);
}
agent[key] = value;
this.setState({
agent: agent,
});
}
submitAgentEdit(willExit) {
const agent = Setting.deepCopy(this.state.agent);
AgentBackend.updateAgent(this.state.owner, this.state.agentName, agent)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully modified"));
if (willExit) {
this.props.history.push("/agents");
} else {
this.setState({
mode: "edit",
owner: agent.owner,
agentName: agent.name,
}, () => {this.getAgent();});
this.props.history.push(`/agents/${agent.owner}/${agent.name}`);
}
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to update")}: ${res.msg}`);
}
})
.catch(error => {
Setting.showMessage("error", `${i18next.t("general:Failed to connect to server")}: ${error}`);
});
}
deleteAgent() {
AgentBackend.deleteAgent(this.state.agent)
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.props.history.push("/agents");
} 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}`);
});
}
renderAgent() {
return (
<Card size="small" title={
<div>
{this.state.mode === "add" ? i18next.t("agent:New Agent") : i18next.t("agent:Edit Agent")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button onClick={() => this.submitAgentEdit(false)}>{i18next.t("general:Save")}</Button>
<Button style={{marginLeft: "20px"}} type="primary" onClick={() => this.submitAgentEdit(true)}>{i18next.t("general:Save & Exit")}</Button>
{this.state.mode === "add" ? <Button style={{marginLeft: "20px"}} onClick={() => this.deleteAgent()}>{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)} value={this.state.agent.owner} onChange={(value => {this.updateAgentField("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}>
{i18next.t("general:Name")}:
</Col>
<Col span={22} >
<Input value={this.state.agent.name} onChange={e => {
this.updateAgentField("name", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{i18next.t("general:Display name")}:
</Col>
<Col span={22} >
<Input value={this.state.agent.displayName} onChange={e => {
this.updateAgentField("displayName", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Listening URL"), i18next.t("general:Listening URL - Tooltip"))} :
</Col>
<Col span={22} >
<Input prefix={<LinkOutlined />} value={this.state.agent.url} onChange={e => {
this.updateAgentField("url", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("token:Access token"), i18next.t("token:Access token - Tooltip"))} :
</Col>
<Col span={22} >
<Input.Password placeholder={"***"} value={this.state.agent.token} onChange={e => {
this.updateAgentField("token", e.target.value);
}} />
</Col>
</Row>
<Row style={{marginTop: "20px"}} >
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}>
{Setting.getLabel(i18next.t("general:Application"), i18next.t("general:Application - Tooltip"))} :
</Col>
<Col span={22} >
<Select virtual={false} style={{width: "100%"}} value={this.state.agent.application} onChange={(value => {this.updateAgentField("application", value);})}>
{
this.state.applications.map((application, index) => <Option key={index} value={application.name}>{application.name}</Option>)
}
</Select>
</Col>
</Row>
</Card>
);
}
render() {
if (this.state.agent === null) {
return null;
}
return (
<div>
{this.renderAgent()}
</div>
);
}
}
export default AgentEditPage;

219
web/src/AgentListPage.js Normal file
View File

@@ -0,0 +1,219 @@
// Copyright 2026 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, Table} from "antd";
import moment from "moment";
import * as Setting from "./Setting";
import * as AgentBackend from "./backend/AgentBackend";
import i18next from "i18next";
import BaseListPage from "./BaseListPage";
import PopconfirmModal from "./common/modal/PopconfirmModal";
class AgentListPage extends BaseListPage {
newAgent() {
const randomName = Setting.getRandomName();
const owner = Setting.getRequestOrganization(this.props.account);
return {
owner: owner,
name: `agent_${randomName}`,
createdTime: moment().format(),
displayName: `New Agent - ${randomName}`,
url: "",
token: "",
application: "",
};
}
addAgent() {
const newAgent = this.newAgent();
AgentBackend.addAgent(newAgent)
.then((res) => {
if (res.status === "ok") {
this.props.history.push({pathname: `/agents/${newAgent.owner}/${newAgent.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}`);
});
}
deleteAgent(i) {
AgentBackend.deleteAgent(this.state.data[i])
.then((res) => {
if (res.status === "ok") {
Setting.showMessage("success", i18next.t("general:Successfully deleted"));
this.fetch({
pagination: {
...this.state.pagination,
current: this.state.pagination.current > 1 && this.state.data.length === 1 ? this.state.pagination.current - 1 : this.state.pagination.current,
},
});
} 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}`);
});
}
fetch = (params = {}) => {
const field = params.searchedColumn, value = params.searchText;
const sortField = params.sortField, sortOrder = params.sortOrder;
if (!params.pagination) {
params.pagination = {current: 1, pageSize: 10};
}
this.setState({loading: true});
AgentBackend.getAgents(Setting.getRequestOrganization(this.props.account), params.pagination.current, params.pagination.pageSize, field, value, sortField, sortOrder)
.then((res) => {
this.setState({loading: false});
if (res.status === "ok") {
this.setState({
data: res.data,
pagination: {
...params.pagination,
total: res.data2,
},
searchText: params.searchText,
searchedColumn: params.searchedColumn,
});
} else {
Setting.showMessage("error", `${i18next.t("general:Failed to get")}: ${res.msg}`);
}
});
};
renderTable(agents) {
const columns = [
{
title: i18next.t("general:Name"),
dataIndex: "name",
key: "name",
width: "160px",
sorter: true,
...this.getColumnSearchProps("name"),
render: (text, record, index) => {
return (
<Link to={`/agents/${record.owner}/${text}`}>
{text}
</Link>
);
},
},
{
title: i18next.t("general:Organization"),
dataIndex: "owner",
key: "owner",
width: "130px",
sorter: true,
...this.getColumnSearchProps("owner"),
},
{
title: i18next.t("general:Created time"),
dataIndex: "createdTime",
key: "createdTime",
width: "180px",
sorter: true,
render: (text, record, index) => {
return Setting.getFormattedDate(text);
},
},
{
title: i18next.t("general:Display name"),
dataIndex: "displayName",
key: "displayName",
sorter: true,
...this.getColumnSearchProps("displayName"),
},
{
title: i18next.t("general:Listening URL"),
dataIndex: "url",
key: "url",
sorter: true,
...this.getColumnSearchProps("url"),
render: (text) => {
if (!text) {
return null;
}
return (
<a target="_blank" rel="noreferrer" href={text}>
{Setting.getShortText(text, 40)}
</a>
);
},
},
{
title: i18next.t("general:Application"),
dataIndex: "application",
key: "application",
width: "140px",
sorter: true,
...this.getColumnSearchProps("application"),
},
{
title: i18next.t("general:Action"),
dataIndex: "op",
key: "op",
width: "180px",
fixed: (Setting.isMobile()) ? false : "right",
render: (text, record, index) => {
return (
<div>
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/agents/${record.owner}/${record.name}`)}>{i18next.t("general:Edit")}</Button>
<PopconfirmModal title={i18next.t("general:Sure to delete") + `: ${record.name} ?`} onConfirm={() => this.deleteAgent(index)}>
</PopconfirmModal>
</div>
);
},
},
];
const filteredColumns = Setting.filterTableColumns(columns, this.props.formItems ?? this.state.formItems);
const paginationProps = {
total: this.state.pagination.total,
showQuickJumper: true,
showSizeChanger: true,
showTotal: () => i18next.t("general:{total} in total").replace("{total}", this.state.pagination.total),
};
return (
<Table
scroll={{x: "max-content"}}
dataSource={agents}
columns={filteredColumns}
rowKey={record => `${record.owner}/${record.name}`}
pagination={{...this.state.pagination, ...paginationProps}}
loading={this.state.loading}
onChange={this.handleTableChange}
size="middle"
bordered
title={() => (
<div>
{i18next.t("general:Agents")}&nbsp;&nbsp;&nbsp;&nbsp;
<Button type="primary" size="small" onClick={() => this.addAgent()}>{i18next.t("general:Add")}</Button>
</div>
)}
/>
);
}
}
export default AgentListPage;

View File

@@ -176,6 +176,7 @@ class App extends Component {
"/organizations", "/groups", "/users", "/invitations", // User Management
"/applications", "/providers", "/resources", "/certs", "/keys", // Identity
"/roles", "/permissions", "/models", "/adapters", "/enforcers", // Authorization
"/servers", "/agents", "/sites", "/rules", // Gateway
"/sessions", "/records", "/tokens", "/verifications", // Logging & Auditing
"/products", "/orders", "/payments", "/plans", "/pricings", "/subscriptions", "/transactions", // Business
"/sysinfo", "/forms", "/syncers", "/webhooks", "/tickets", "/swagger", // Admin
@@ -217,6 +218,16 @@ class App extends Component {
}
} else if (uri.includes("/keys")) {
return "/keys";
} else if (uri.includes("/servers") || uri.includes("/agents") || uri.includes("/sites") || uri.includes("/rules")) {
if (uri.includes("/servers")) {
return "/servers";
} else if (uri.includes("/agents")) {
return "/agents";
} else if (uri.includes("/sites")) {
return "/sites";
} else if (uri.includes("/rules")) {
return "/rules";
}
} else if (uri.includes("/roles") || uri.includes("/permissions") || uri.includes("/models") || uri.includes("/adapters") || uri.includes("/enforcers")) {
if (uri.includes("/roles")) {
return "/roles";
@@ -297,6 +308,8 @@ class App extends Component {
this.setState({selectedMenuKey: "/orgs"});
} else if (uri.includes("/applications") || uri.includes("/providers") || uri.includes("/resources") || uri.includes("/certs") || uri.includes("/keys")) {
this.setState({selectedMenuKey: "/identity"});
} else if (uri.includes("/servers") || uri.includes("/agents") || uri.includes("/sites") || uri.includes("/rules")) {
this.setState({selectedMenuKey: "/gateway"});
} else if (uri.includes("/roles") || uri.includes("/permissions") || uri.includes("/models") || uri.includes("/adapters") || uri.includes("/enforcers")) {
this.setState({selectedMenuKey: "/auth"});
} else if (uri.includes("/records") || uri.includes("/tokens") || uri.includes("/sessions") || uri.includes("/verifications")) {

View File

@@ -110,6 +110,8 @@ import SiteListPage from "./SiteListPage";
import SiteEditPage from "./SiteEditPage";
import ServerListPage from "./ServerListPage";
import ServerEditPage from "./ServerEditPage";
import AgentListPage from "./AgentListPage";
import AgentEditPage from "./AgentEditPage";
import RuleEditPage from "./RuleEditPage";
import RuleListPage from "./RuleListPage";
@@ -352,6 +354,7 @@ function ManagementPage(props) {
res.push(Setting.getItem(<Link style={{color: textColor}} to="/sites">{i18next.t("general:Gateway")}</Link>, "/gateway", <CheckCircleTwoTone twoToneColor={twoToneColor} />, [
Setting.getItem(<Link to="/servers">{i18next.t("general:MCP Servers")}</Link>, "/servers"),
Setting.getItem(<Link to="/agents">{i18next.t("general:Agents")}</Link>, "/agents"),
Setting.getItem(<Link to="/sites">{i18next.t("general:Sites")}</Link>, "/sites"),
Setting.getItem(<Link to="/certs">{i18next.t("general:Certs")}</Link>, "/certs"),
Setting.getItem(<Link to="/rules">{i18next.t("general:Rules")}</Link>, "/rules"),
@@ -498,6 +501,8 @@ function ManagementPage(props) {
<Route exact path="/keys/:organizationName/:keyName" render={(props) => renderLoginIfNotLoggedIn(<KeyEditPage account={account} {...props} />)} />
<Route exact path="/servers" render={(props) => renderLoginIfNotLoggedIn(<ServerListPage account={account} {...props} />)} />
<Route exact path="/servers/:organizationName/:serverName" render={(props) => renderLoginIfNotLoggedIn(<ServerEditPage account={account} {...props} />)} />
<Route exact path="/agents" render={(props) => renderLoginIfNotLoggedIn(<AgentListPage account={account} {...props} />)} />
<Route exact path="/agents/:organizationName/:agentName" render={(props) => renderLoginIfNotLoggedIn(<AgentEditPage account={account} {...props} />)} />
<Route exact path="/sites" render={(props) => renderLoginIfNotLoggedIn(<SiteListPage account={account} {...props} />)} />
<Route exact path="/sites/:organizationName/:siteName" render={(props) => renderLoginIfNotLoggedIn(<SiteEditPage account={account} {...props} />)} />
<Route exact path="/rules" render={(props) => renderLoginIfNotLoggedIn(<RuleListPage account={account} {...props} />)} />

View File

@@ -0,0 +1,56 @@
// Copyright 2026 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 * as Setting from "../Setting";
export function getAgents(owner, page = "", pageSize = "", field = "", value = "", sortField = "", sortOrder = "") {
return fetch(`${Setting.ServerUrl}/api/get-agents?owner=${owner}&p=${page}&pageSize=${pageSize}&field=${field}&value=${value}&sortField=${sortField}&sortOrder=${sortOrder}`, {
method: "GET",
credentials: "include",
}).then(res => res.json());
}
export function getAgent(owner, name) {
return fetch(`${Setting.ServerUrl}/api/get-agent?id=${owner}/${encodeURIComponent(name)}`, {
method: "GET",
credentials: "include",
}).then(res => res.json());
}
export function updateAgent(owner, name, agent) {
const newAgent = Setting.deepCopy(agent);
return fetch(`${Setting.ServerUrl}/api/update-agent?id=${owner}/${encodeURIComponent(name)}`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newAgent),
}).then(res => res.json());
}
export function addAgent(agent) {
const newAgent = Setting.deepCopy(agent);
return fetch(`${Setting.ServerUrl}/api/add-agent`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newAgent),
}).then(res => res.json());
}
export function deleteAgent(agent) {
const newAgent = Setting.deepCopy(agent);
return fetch(`${Setting.ServerUrl}/api/delete-agent`, {
method: "POST",
credentials: "include",
body: JSON.stringify(newAgent),
}).then(res => res.json());
}

View File

@@ -45,6 +45,7 @@ export const NavItemTree = ({disabled, checkedKeys, defaultExpandedKeys, onCheck
{title: i18next.t("general:Rules"), key: "/rules"},
{title: i18next.t("general:Sites"), key: "/sites"},
{title: i18next.t("general:MCP Servers"), key: "/servers"},
{title: i18next.t("general:Agents"), key: "/agents"},
],
},
{