@@ -0,0 +1,109 @@ | |||
// Copyright 2023 The casbin 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/casbin/casibase/object" | |||
) | |||
func (c *ApiController) GetGlobalTasks() { | |||
tasks, err := object.GetGlobalTasks() | |||
if err != nil { | |||
c.ResponseError(err.Error()) | |||
return | |||
} | |||
c.ResponseOk(object.GetMaskedTasks(tasks, true)) | |||
} | |||
func (c *ApiController) GetTasks() { | |||
owner := "admin" | |||
tasks, err := object.GetTasks(owner) | |||
if err != nil { | |||
c.ResponseError(err.Error()) | |||
return | |||
} | |||
c.ResponseOk(object.GetMaskedTasks(tasks, true)) | |||
} | |||
func (c *ApiController) GetTask() { | |||
id := c.Input().Get("id") | |||
task, err := object.GetTask(id) | |||
if err != nil { | |||
c.ResponseError(err.Error()) | |||
return | |||
} | |||
c.ResponseOk(object.GetMaskedTask(task, true)) | |||
} | |||
func (c *ApiController) UpdateTask() { | |||
id := c.Input().Get("id") | |||
var task object.Task | |||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &task) | |||
if err != nil { | |||
c.ResponseError(err.Error()) | |||
return | |||
} | |||
success, err := object.UpdateTask(id, &task) | |||
if err != nil { | |||
c.ResponseError(err.Error()) | |||
return | |||
} | |||
c.ResponseOk(success) | |||
} | |||
func (c *ApiController) AddTask() { | |||
var task object.Task | |||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &task) | |||
if err != nil { | |||
c.ResponseError(err.Error()) | |||
return | |||
} | |||
task.Owner = "admin" | |||
success, err := object.AddTask(&task) | |||
if err != nil { | |||
c.ResponseError(err.Error()) | |||
return | |||
} | |||
c.ResponseOk(success) | |||
} | |||
func (c *ApiController) DeleteTask() { | |||
var task object.Task | |||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &task) | |||
if err != nil { | |||
c.ResponseError(err.Error()) | |||
return | |||
} | |||
success, err := object.DeleteTask(&task) | |||
if err != nil { | |||
c.ResponseError(err.Error()) | |||
return | |||
} | |||
c.ResponseOk(success) | |||
} |
@@ -141,4 +141,9 @@ func (a *Adapter) createTable() { | |||
if err != nil { | |||
panic(err) | |||
} | |||
err = a.engine.Sync2(new(Task)) | |||
if err != nil { | |||
panic(err) | |||
} | |||
} |
@@ -27,9 +27,10 @@ type Task struct { | |||
CreatedTime string `xorm:"varchar(100)" json:"createdTime"` | |||
DisplayName string `xorm:"varchar(100)" json:"displayName"` | |||
Application string `xorm:"varchar(100)" json:"application"` | |||
Provider string `xorm:"varchar(100)" json:"provider"` | |||
Application string `xorm:"varchar(100)" json:"application"` | |||
Path string `xorm:"varchar(100)" json:"path"` | |||
Log string `xorm:"mediumtext" json:"log"` | |||
} | |||
func GetMaskedTask(task *Task, isMaskEnabled bool) *Task { | |||
@@ -98,6 +98,13 @@ func initAPI() { | |||
beego.Router("/api/add-message", &controllers.ApiController{}, "POST:AddMessage") | |||
beego.Router("/api/delete-message", &controllers.ApiController{}, "POST:DeleteMessage") | |||
beego.Router("/api/get-global-tasks", &controllers.ApiController{}, "GET:GetGlobalTasks") | |||
beego.Router("/api/get-tasks", &controllers.ApiController{}, "GET:GetTasks") | |||
beego.Router("/api/get-task", &controllers.ApiController{}, "GET:GetTask") | |||
beego.Router("/api/update-task", &controllers.ApiController{}, "POST:UpdateTask") | |||
beego.Router("/api/add-task", &controllers.ApiController{}, "POST:AddTask") | |||
beego.Router("/api/delete-task", &controllers.ApiController{}, "POST:DeleteTask") | |||
beego.Router("/api/update-file", &controllers.ApiController{}, "POST:UpdateFile") | |||
beego.Router("/api/add-file", &controllers.ApiController{}, "POST:AddFile") | |||
beego.Router("/api/delete-file", &controllers.ApiController{}, "POST:DeleteFile") | |||
@@ -45,6 +45,8 @@ import ChatEditPage from "./ChatEditPage"; | |||
import ChatListPage from "./ChatListPage"; | |||
import MessageListPage from "./MessageListPage"; | |||
import MessageEditPage from "./MessageEditPage"; | |||
import TaskListPage from "./TaskListPage"; | |||
import TaskEditPage from "./TaskEditPage"; | |||
import ChatPage from "./ChatPage"; | |||
const {Header, Footer, Content} = Layout; | |||
@@ -104,6 +106,8 @@ class App extends Component { | |||
this.setState({selectedMenuKey: "/chats"}); | |||
} else if (uri.includes("/messages")) { | |||
this.setState({selectedMenuKey: "/messages"}); | |||
} else if (uri.includes("/tasks")) { | |||
this.setState({selectedMenuKey: "/tasks"}); | |||
} else { | |||
this.setState({selectedMenuKey: "null"}); | |||
} | |||
@@ -303,6 +307,9 @@ class App extends Component { | |||
res.push(Setting.getItem(<Link to="/messages">{i18next.t("general:Messages")}</Link>, | |||
"/messages")); | |||
res.push(Setting.getItem(<Link to="/tasks">{i18next.t("general:Tasks")}</Link>, | |||
"/tasks")); | |||
if (Setting.isLocalAdminUser(this.state.account)) { | |||
res.push(Setting.getItem( | |||
<a target="_blank" rel="noreferrer" href={Setting.getMyProfileUrl(this.state.account).replace("/account", "/resources")}> | |||
@@ -374,6 +381,8 @@ class App extends Component { | |||
<Route exact path="/chats/:chatName" render={(props) => this.renderSigninIfNotSignedIn(<ChatEditPage account={this.state.account} {...props} />)} /> | |||
<Route exact path="/messages" render={(props) => this.renderSigninIfNotSignedIn(<MessageListPage account={this.state.account} {...props} />)} /> | |||
<Route exact path="/messages/:messageName" render={(props) => this.renderSigninIfNotSignedIn(<MessageEditPage account={this.state.account} {...props} />)} /> | |||
<Route exact path="/tasks" render={(props) => this.renderSigninIfNotSignedIn(<TaskListPage account={this.state.account} {...props} />)} /> | |||
<Route exact path="/tasks/:taskName" render={(props) => this.renderSigninIfNotSignedIn(<TaskEditPage account={this.state.account} {...props} />)} /> | |||
<Route exact path="/chat" render={(props) => this.renderSigninIfNotSignedIn(<ChatPage account={this.state.account} {...props} />)} /> | |||
</Switch> | |||
); | |||
@@ -0,0 +1,187 @@ | |||
// Copyright 2023 The casbin 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 * as TaskBackend from "./backend/TaskBackend"; | |||
import * as Setting from "./Setting"; | |||
import i18next from "i18next"; | |||
import * as ProviderBackend from "./backend/ProviderBackend"; | |||
const {Option} = Select; | |||
class TaskEditPage extends React.Component { | |||
constructor(props) { | |||
super(props); | |||
this.state = { | |||
classes: props, | |||
taskName: props.match.params.taskName, | |||
modelProviders: [], | |||
task: null, | |||
}; | |||
} | |||
UNSAFE_componentWillMount() { | |||
this.getTask(); | |||
this.getModelProviders(); | |||
} | |||
getTask() { | |||
TaskBackend.getTask(this.props.account.name, this.state.taskName) | |||
.then((res) => { | |||
if (res.status === "ok") { | |||
this.setState({ | |||
task: res.data, | |||
}); | |||
} else { | |||
Setting.showMessage("error", `Failed to get task: ${res.msg}`); | |||
} | |||
}); | |||
} | |||
getModelProviders() { | |||
ProviderBackend.getProviders(this.props.account.name) | |||
.then((res) => { | |||
if (res.status === "ok") { | |||
this.setState({ | |||
modelProviders: res.data.filter(provider => provider.category === "Model"), | |||
}); | |||
} else { | |||
Setting.showMessage("error", `Failed to get providers: ${res.msg}`); | |||
} | |||
}); | |||
} | |||
parseTaskField(key, value) { | |||
if ([""].includes(key)) { | |||
value = Setting.myParseInt(value); | |||
} | |||
return value; | |||
} | |||
updateTaskField(key, value) { | |||
value = this.parseTaskField(key, value); | |||
const task = this.state.task; | |||
task[key] = value; | |||
this.setState({ | |||
task: task, | |||
}); | |||
} | |||
renderTask() { | |||
return ( | |||
<Card size="small" title={ | |||
<div> | |||
{i18next.t("task:Edit Task")} | |||
<Button type="primary" onClick={this.submitTaskEdit.bind(this)}>{i18next.t("general:Save")}</Button> | |||
</div> | |||
} style={{marginLeft: "5px"}} type="inner"> | |||
<Row style={{marginTop: "10px"}} > | |||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> | |||
{i18next.t("general:Name")}: | |||
</Col> | |||
<Col span={22} > | |||
<Input value={this.state.task.name} onChange={e => { | |||
this.updateTaskField("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.task.displayName} onChange={e => { | |||
this.updateTaskField("displayName", e.target.value); | |||
}} /> | |||
</Col> | |||
</Row> | |||
<Row style={{marginTop: "20px"}} > | |||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> | |||
{i18next.t("store:Model provider")}: | |||
</Col> | |||
<Col span={22} > | |||
<Select virtual={false} style={{width: "100%"}} value={this.state.task.provider} onChange={(value => {this.updateTaskField("provider", value);})} | |||
options={this.state.modelProviders.map((provider) => Setting.getOption(`${provider.displayName} (${provider.name})`, `${provider.name}`)) | |||
} /> | |||
</Col> | |||
</Row> | |||
<Row style={{marginTop: "20px"}} > | |||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> | |||
{i18next.t("task:Application")}: | |||
</Col> | |||
<Col span={22} > | |||
<Select virtual={false} style={{width: "100%"}} value={this.state.task.application} onChange={(value => {this.updateTaskField("application", value);})}> | |||
{ | |||
[ | |||
{id: "Docs-Polish", name: "Docs-Polish"}, | |||
].map((item, index) => <Option key={index} value={item.id}>{item.name}</Option>) | |||
} | |||
</Select> | |||
</Col> | |||
</Row> | |||
<Row style={{marginTop: "20px"}} > | |||
<Col style={{marginTop: "5px"}} span={(Setting.isMobile()) ? 22 : 2}> | |||
{i18next.t("task:Path")}: | |||
</Col> | |||
<Col span={22} > | |||
<Input value={this.state.task.path} onChange={e => { | |||
this.updateTaskField("path", e.target.value); | |||
}} /> | |||
</Col> | |||
</Row> | |||
</Card> | |||
); | |||
} | |||
submitTaskEdit() { | |||
const task = Setting.deepCopy(this.state.task); | |||
TaskBackend.updateTask(this.state.task.owner, this.state.taskName, task) | |||
.then((res) => { | |||
if (res.status === "ok") { | |||
if (res.data) { | |||
Setting.showMessage("success", "Successfully saved"); | |||
this.setState({ | |||
taskName: this.state.task.name, | |||
}); | |||
this.props.history.push(`/tasks/${this.state.task.name}`); | |||
} else { | |||
Setting.showMessage("error", "failed to save: server side failure"); | |||
this.updateTaskField("name", this.state.taskName); | |||
} | |||
} else { | |||
Setting.showMessage("error", `failed to save: ${res.msg}`); | |||
} | |||
}) | |||
.catch(error => { | |||
Setting.showMessage("error", `failed to save: ${error}`); | |||
}); | |||
} | |||
render() { | |||
return ( | |||
<div> | |||
{ | |||
this.state.task !== null ? this.renderTask() : null | |||
} | |||
<div style={{marginTop: "20px", marginLeft: "40px"}}> | |||
<Button type="primary" size="large" onClick={this.submitTaskEdit.bind(this)}>{i18next.t("general:Save")}</Button> | |||
</div> | |||
</div> | |||
); | |||
} | |||
} | |||
export default TaskEditPage; |
@@ -0,0 +1,197 @@ | |||
// Copyright 2023 The casbin 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, Popconfirm, Table} from "antd"; | |||
import moment from "moment"; | |||
import * as Setting from "./Setting"; | |||
import * as TaskBackend from "./backend/TaskBackend"; | |||
import i18next from "i18next"; | |||
class TaskListPage extends React.Component { | |||
constructor(props) { | |||
super(props); | |||
this.state = { | |||
classes: props, | |||
tasks: null, | |||
}; | |||
} | |||
UNSAFE_componentWillMount() { | |||
this.getTasks(); | |||
} | |||
getTasks() { | |||
TaskBackend.getTasks(this.props.account.name) | |||
.then((res) => { | |||
if (res.status === "ok") { | |||
this.setState({ | |||
tasks: res.data, | |||
}); | |||
} else { | |||
Setting.showMessage("error", `Failed to get tasks: ${res.msg}`); | |||
} | |||
}); | |||
} | |||
newTask() { | |||
const randomName = Setting.getRandomName(); | |||
return { | |||
owner: this.props.account.name, | |||
name: `task_${randomName}`, | |||
createdTime: moment().format(), | |||
displayName: `New Task - ${randomName}`, | |||
provider: "provider_openai", | |||
application: "Docs-Polish", | |||
path: "F:/github_repos/casdoor-website", | |||
}; | |||
} | |||
addTask() { | |||
const newTask = this.newTask(); | |||
TaskBackend.addTask(newTask) | |||
.then((res) => { | |||
if (res.status === "ok") { | |||
Setting.showMessage("success", "Task added successfully"); | |||
this.setState({ | |||
tasks: Setting.prependRow(this.state.tasks, newTask), | |||
}); | |||
} else { | |||
Setting.showMessage("error", `Failed to add task: ${res.msg}`); | |||
} | |||
}) | |||
.catch(error => { | |||
Setting.showMessage("error", `Task failed to add: ${error}`); | |||
}); | |||
} | |||
deleteTask(i) { | |||
TaskBackend.deleteTask(this.state.tasks[i]) | |||
.then((res) => { | |||
if (res.status === "ok") { | |||
Setting.showMessage("success", "Task deleted successfully"); | |||
this.setState({ | |||
tasks: Setting.deleteRow(this.state.tasks, i), | |||
}); | |||
} else { | |||
Setting.showMessage("error", `Task failed to delete: ${res.msg}`); | |||
} | |||
}) | |||
.catch(error => { | |||
Setting.showMessage("error", `Task failed to delete: ${error}`); | |||
}); | |||
} | |||
renderTable(tasks) { | |||
const columns = [ | |||
{ | |||
title: i18next.t("general:Name"), | |||
dataIndex: "name", | |||
key: "name", | |||
width: "160px", | |||
sorter: (a, b) => a.name.localeCompare(b.name), | |||
render: (text, record, index) => { | |||
return ( | |||
<Link to={`/tasks/${text}`}> | |||
{text} | |||
</Link> | |||
); | |||
}, | |||
}, | |||
{ | |||
title: i18next.t("general:Display name"), | |||
dataIndex: "displayName", | |||
key: "displayName", | |||
width: "200px", | |||
sorter: (a, b) => a.displayName.localeCompare(b.displayName), | |||
}, | |||
{ | |||
title: i18next.t("store:Model provider"), | |||
dataIndex: "provider", | |||
key: "provider", | |||
width: "250px", | |||
sorter: (a, b) => a.provider.localeCompare(b.provider), | |||
render: (text, record, index) => { | |||
return ( | |||
<Link to={`/providers/${text}`}> | |||
{text} | |||
</Link> | |||
); | |||
}, | |||
}, | |||
{ | |||
title: i18next.t("task:Application"), | |||
dataIndex: "application", | |||
key: "application", | |||
width: "180px", | |||
sorter: (a, b) => a.application.localeCompare(b.application), | |||
}, | |||
{ | |||
title: i18next.t("task:Path"), | |||
dataIndex: "path", | |||
key: "path", | |||
// width: "160px", | |||
sorter: (a, b) => a.path.localeCompare(b.path), | |||
}, | |||
{ | |||
title: i18next.t("general:Action"), | |||
dataIndex: "action", | |||
key: "action", | |||
width: "180px", | |||
render: (text, record, index) => { | |||
return ( | |||
<div> | |||
<Button style={{marginTop: "10px", marginBottom: "10px", marginRight: "10px"}} type="primary" onClick={() => this.props.history.push(`/tasks/${record.name}`)}>{i18next.t("general:Edit")}</Button> | |||
<Popconfirm | |||
title={`Sure to delete task: ${record.name} ?`} | |||
onConfirm={() => this.deleteTask(index)} | |||
okText="OK" | |||
cancelText="Cancel" | |||
> | |||
<Button style={{marginBottom: "10px"}} type="primary" danger>{i18next.t("general:Delete")}</Button> | |||
</Popconfirm> | |||
</div> | |||
); | |||
}, | |||
}, | |||
]; | |||
return ( | |||
<div> | |||
<Table scroll={{x: "max-content"}} columns={columns} dataSource={tasks} rowKey="name" size="middle" bordered pagination={{pageSize: 100}} | |||
title={() => ( | |||
<div> | |||
{i18next.t("general:Tasks")} | |||
<Button type="primary" size="small" onClick={this.addTask.bind(this)}>{i18next.t("general:Add")}</Button> | |||
</div> | |||
)} | |||
loading={tasks === null} | |||
/> | |||
</div> | |||
); | |||
} | |||
render() { | |||
return ( | |||
<div> | |||
{ | |||
this.renderTable(this.state.tasks) | |||
} | |||
</div> | |||
); | |||
} | |||
} | |||
export default TaskListPage; |
@@ -0,0 +1,63 @@ | |||
// Copyright 2023 The casbin 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 getGlobalTasks() { | |||
return fetch(`${Setting.ServerUrl}/api/get-global-tasks`, { | |||
method: "GET", | |||
credentials: "include", | |||
}).then(res => res.json()); | |||
} | |||
export function getTasks(owner) { | |||
return fetch(`${Setting.ServerUrl}/api/get-tasks?owner=${owner}`, { | |||
method: "GET", | |||
credentials: "include", | |||
}).then(res => res.json()); | |||
} | |||
export function getTask(owner, name) { | |||
return fetch(`${Setting.ServerUrl}/api/get-task?id=${owner}/${encodeURIComponent(name)}`, { | |||
method: "GET", | |||
credentials: "include", | |||
}).then(res => res.json()); | |||
} | |||
export function updateTask(owner, name, task) { | |||
const newTask = Setting.deepCopy(task); | |||
return fetch(`${Setting.ServerUrl}/api/update-task?id=${owner}/${encodeURIComponent(name)}`, { | |||
method: "POST", | |||
credentials: "include", | |||
body: JSON.stringify(newTask), | |||
}).then(res => res.json()); | |||
} | |||
export function addTask(task) { | |||
const newTask = Setting.deepCopy(task); | |||
return fetch(`${Setting.ServerUrl}/api/add-task`, { | |||
method: "POST", | |||
credentials: "include", | |||
body: JSON.stringify(newTask), | |||
}).then(res => res.json()); | |||
} | |||
export function deleteTask(task) { | |||
const newTask = Setting.deepCopy(task); | |||
return fetch(`${Setting.ServerUrl}/api/delete-task`, { | |||
method: "POST", | |||
credentials: "include", | |||
body: JSON.stringify(newTask), | |||
}).then(res => res.json()); | |||
} |